Compare commits

..

5 Commits

Author SHA1 Message Date
Drew Ritter
15d3b46614 Align Pi mapping with action vocabulary 2026-05-13 17:53:35 -07:00
Drew Ritter
c9116738e0 Bump evals submodule for Pi backend 2026-05-13 17:53:31 -07:00
Jesse Vincent
83a00caef9 chore: keep pi extension under .pi 2026-05-13 17:51:47 -07:00
Jesse Vincent
a1e1cd4ece feat: add pi superpowers package extension 2026-05-13 17:51:47 -07:00
Jesse Vincent
dd0db70312 docs: plan pi extension and evals work 2026-05-13 17:50:03 -07:00
10 changed files with 58 additions and 1231 deletions

View File

@@ -21,7 +21,6 @@
"workflow"
],
"skills": "./skills/",
"hooks": "./hooks/hooks-codex.json",
"interface": {
"displayName": "Superpowers",
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",

View File

@@ -86,18 +86,6 @@ Superpowers is available via the [official Codex plugin marketplace](https://git
- Select `Install Plugin`.
#### Automatic startup bootstrap
Codex plugin hooks are still gated behind Codex's `plugin_hooks` feature. To opt in:
```bash
codex features enable plugin_hooks
```
Restart Codex, open `/hooks`, review the Superpowers `SessionStart` hook, and trust it. Codex may require you to re-review the hook after Superpowers updates if the hook definition changes.
Fallback: if `plugin_hooks` is disabled, unavailable, or untrusted, Superpowers still installs as a normal Codex plugin and the skills remain available. The automatic startup bootstrap is the part that waits for the trusted hook.
### Cursor
- In Cursor Agent chat, install from marketplace:

View File

@@ -1,369 +0,0 @@
# Codex Native Hooks Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Give Codex CLI users native Superpowers startup behavior through trusted Codex plugin hooks, keep the current Codex plugin/skills path as a fallback while `plugin_hooks` is gated, and prepare Codex App user-facing parity behind a visible `/hooks` UI smoke test.
**Architecture:** Add a Codex-specific hook manifest that points Codex at the existing shared `hooks/session-start` implementation through the existing cross-platform `hooks/run-hook.cmd` wrapper. Package the hook files into the official Codex plugin sync output. Keep Claude Code's existing `hooks/hooks.json`, Cursor's `hooks/hooks-cursor.json`, and Copilot/unknown-platform output behavior unchanged.
**Tech Stack:** Bash hook scripts, JSON plugin manifests, shell regression tests, Node.js only for JSON parsing in tests, Codex CLI/app-server smoke verification. No new runtime dependencies.
---
## Source Map
- Spec: `docs/superpowers/specs/2026-05-12-codex-native-hooks-design.md`
- Codex plugin manifest: `.codex-plugin/plugin.json`
- Claude hook manifest: `hooks/hooks.json`
- Cursor hook manifest: `hooks/hooks-cursor.json`
- New Codex hook manifest: `hooks/hooks-codex.json`
- Shared hook wrapper: `hooks/run-hook.cmd`
- Shared hook implementation: `hooks/session-start`
- Codex plugin sync script: `scripts/sync-to-codex-plugin.sh`
- Codex sync regression test: `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh`
- New hook output regression test: `tests/hooks/test-session-start.sh`
- User docs: `README.md`
- Windows hook docs: `docs/windows/polyglot-hooks.md`
## Locked Decisions
- Use `hooks/hooks-codex.json` for Codex instead of changing the shared Claude `hooks/hooks.json`.
- Do not add `resume` to the Claude matcher until Claude Code is explicitly verified to accept it.
- Use Codex's verified `${PLUGIN_ROOT}` placeholder in the Codex manifest command.
- Do not use or document `CODEX_PLUGIN_ROOT`; Codex plugin hooks provide `PLUGIN_ROOT`, `CLAUDE_PLUGIN_ROOT`, `PLUGIN_DATA`, and `CLAUDE_PLUGIN_DATA`.
- Do not auto-trust hooks from Superpowers. Users trust executable hooks through Codex's `/hooks` UI.
- Keep fallback behavior: Codex users without enabled/trusted plugin hooks still get installed skills, but not automatic startup bootstrap.
- Treat App runtime support as verified through the bundled app-server, but only claim App user-facing parity after a visible App `/hooks` UI smoke test.
## Task 1: Baseline And Contract Check
- [ ] Run:
```bash
git status --short --branch
codex --version
codex features list
/Applications/Codex.app/Contents/Resources/codex --version
```
- [ ] Expected observations:
- Worktree status is understood before edits.
- Local Codex is at least `0.130.0`.
- `plugin_hooks` exists and is still under development/default-off, or any drift is recorded in the implementation notes.
- App-bundled Codex can be invoked, or the App smoke task is marked blocked with the exact command error.
- [ ] Read the files in the Source Map before editing.
## Task 2: Add SessionStart Output Regression Tests
- [ ] Create `tests/hooks/test-session-start.sh`.
- [ ] The test file must:
- Run from any current working directory.
- Use a temporary `HOME` for each scenario.
- Invoke the real `hooks/session-start`.
- Parse JSON with Node.js from stdin; do not add npm dependencies.
- Fail on invalid JSON, empty injected context, or platform output shape drift.
- [ ] Cover these scenarios:
- Claude Code: `CLAUDE_PLUGIN_ROOT="$REPO_ROOT"` emits `hookSpecificOutput.hookEventName = "SessionStart"` and a non-empty `hookSpecificOutput.additionalContext`.
- Codex plugin hooks: `PLUGIN_DATA="$tmp/data" CLAUDE_PLUGIN_DATA="$tmp/data" PLUGIN_ROOT="$REPO_ROOT" CLAUDE_PLUGIN_ROOT="$REPO_ROOT"` emits the same nested `hookSpecificOutput.additionalContext` shape.
- Cursor: `CURSOR_PLUGIN_ROOT="$REPO_ROOT" CLAUDE_PLUGIN_ROOT="$REPO_ROOT"` emits top-level `additional_context` and does not also emit `hookSpecificOutput`.
- Copilot CLI: `COPILOT_CLI=1 CLAUDE_PLUGIN_ROOT="$REPO_ROOT"` emits top-level `additionalContext` and does not also emit `hookSpecificOutput`.
- Claude legacy warning: with `$HOME/.config/superpowers/skills` present and `CLAUDE_PLUGIN_ROOT="$REPO_ROOT"`, the injected context still contains the existing Claude migration guidance to `~/.claude/skills`.
- Codex legacy warning: with `$HOME/.config/superpowers/skills` present and the Codex plugin-hook environment, the injected context does not mention `~/.claude/skills` or "Claude Code's skills system" and instead uses harness-neutral custom-skill wording.
- [ ] Run the test before changing `hooks/session-start`:
```bash
bash tests/hooks/test-session-start.sh
```
- [ ] Expected result before implementation: the Codex legacy warning scenario fails because the current shared hook sees `CLAUDE_PLUGIN_ROOT` and emits Claude-specific migration text.
## Task 3: Make Legacy Warning Text Harness-Aware
- [ ] Edit `hooks/session-start`.
- [ ] Preserve these existing behaviors:
- The script derives the plugin root from its own location.
- The script reads `skills/using-superpowers/SKILL.md`.
- Cursor receives only top-level `additional_context`.
- Claude receives only nested `hookSpecificOutput.additionalContext`.
- Codex receives the same nested shape as Claude, because the verified Codex parser accepts it and Codex sets `CLAUDE_PLUGIN_ROOT`.
- Copilot and unknown platforms receive only top-level `additionalContext`.
- [ ] Add Codex detection before building the legacy warning. Use Codex data env vars, not the locally derived `PLUGIN_ROOT` variable:
```bash
is_codex_hook=0
if [ -n "${PLUGIN_DATA:-}" ] || [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then
is_codex_hook=1
fi
```
- [ ] Keep the current Claude legacy warning text for non-Codex Claude.
- [ ] For Codex hook runs, use this neutral warning content when `$HOME/.config/superpowers/skills` exists:
```text
WARNING: Superpowers now uses your coding agent's native skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to a skills location supported by your coding agent. To make this message go away, remove ~/.config/superpowers/skills
```
- [ ] Re-run:
```bash
bash tests/hooks/test-session-start.sh
```
- [ ] Expected result after implementation: all session-start scenarios pass.
## Task 4: Add Codex Hook Manifest
- [ ] Add `hooks/hooks-codex.json`:
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
}
]
}
]
}
}
```
- [ ] Edit `.codex-plugin/plugin.json` and add:
```json
"hooks": "./hooks/hooks-codex.json"
```
- [ ] Keep `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `hooks/hooks.json`, and `hooks/hooks-cursor.json` unchanged unless a test proves they must change.
- [ ] Validate JSON:
```bash
node -e 'for (const f of [".codex-plugin/plugin.json","hooks/hooks-codex.json","hooks/hooks.json","hooks/hooks-cursor.json"]) JSON.parse(require("fs").readFileSync(f,"utf8"));'
```
## Task 5: Package Hooks In Codex Plugin Sync
- [ ] Edit `scripts/sync-to-codex-plugin.sh`.
- [ ] Remove the `/hooks/` entry from `EXCLUDES`.
- [ ] Do not broaden the sync script beyond this packaging change.
- [ ] Edit `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh` fixtures:
- In `write_upstream_fixture`, create:
- `hooks/hooks-codex.json`
- `hooks/session-start`
- `hooks/run-hook.cmd`
- Add those files to the upstream fixture commit.
- In `write_synced_destination_fixture`, create and commit the same hook files under `plugins/superpowers/hooks/` so the clean no-op apply scenario stays clean.
- [ ] Add preview assertions:
- Preview includes `hooks/hooks-codex.json`.
- Preview includes `hooks/session-start`.
- Preview includes `hooks/run-hook.cmd`.
- [ ] Add clean no-op assertion:
- Clean no-op local apply reports no changes when hook files are already synced.
- [ ] Run:
```bash
bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
```
- [ ] Expected result: all existing sync assertions still pass, and the new hook packaging assertions pass.
## Task 6: Update README Codex Docs
- [ ] Edit `README.md`.
- [ ] In `### Codex CLI`, keep the existing marketplace install steps and add a short optional preview subsection after install:
````markdown
#### Automatic startup bootstrap
Codex plugin hooks are still gated behind Codex's `plugin_hooks` feature. To opt in:
```bash
codex features enable plugin_hooks
```
Restart Codex, open `/hooks`, review the Superpowers `SessionStart` hook, and trust it. Codex will ask you to re-review the hook after Superpowers updates if the hook definition changes.
If `plugin_hooks` is disabled, unavailable, or untrusted, Superpowers still installs as a normal Codex plugin and the skills remain available. The automatic startup bootstrap is the part that waits for the trusted hook.
````
- [ ] In `### Codex App`, keep the existing install steps.
- [ ] If the visible App `/hooks` UI smoke in Task 9 passes during implementation, add the same automatic startup bootstrap subsection to `### Codex App`, with the enablement step described as the persisted Codex config path:
```bash
codex features enable plugin_hooks
```
- [ ] If the visible App `/hooks` UI smoke is blocked or fails, do not claim automatic App startup bootstrap in the README. Record the App smoke status in the implementation summary instead.
- [ ] README wording must not imply automatic startup for Codex users who have not enabled and trusted plugin hooks.
## Task 7: Update Windows Hook Docs
- [ ] Edit `docs/windows/polyglot-hooks.md`.
- [ ] Update stale references:
- Replace `session-start.cmd` with `run-hook.cmd`.
- Replace `session-start.sh` with extensionless `session-start`.
- Show `hooks/hooks-codex.json` as the Codex-specific manifest.
- Keep `hooks/hooks.json` as the Claude Code manifest.
- Mention Codex uses `${PLUGIN_ROOT}` and `startup|resume|clear`.
- [ ] The docs must describe the current file structure:
```text
hooks/
|-- hooks.json
|-- hooks-codex.json
|-- hooks-cursor.json
|-- run-hook.cmd
`-- session-start
```
- [ ] Update the Windows simulation command to:
```powershell
$env:CLAUDE_PLUGIN_ROOT = "C:\path\to\plugin"
cmd /c "C:\path\to\plugin\hooks\run-hook.cmd session-start"
```
- [ ] Do not add Windows-specific runtime dependencies.
## Task 8: Run Codex CLI Native Hook Smoke
- [ ] Build a temporary isolated Codex home and staged plugin from the working tree:
```bash
SMOKE_HOME="$(mktemp -d)"
SMOKE_PLUGIN="$SMOKE_HOME/plugins/cache/debug/superpowers/local"
mkdir -p "$SMOKE_PLUGIN"
rsync -a --exclude .git ./ "$SMOKE_PLUGIN/"
```
- [ ] Use persisted feature config, not undocumented root CLI feature flags:
```bash
cat > "$SMOKE_HOME/config.toml" <<'EOF'
[features]
plugins = true
hooks = true
plugin_hooks = true
[plugins."superpowers@debug"]
enabled = true
EOF
```
- [ ] Run Codex `app-server --listen stdio://` against the temp home and call `hooks/list`:
```bash
CODEX_HOME="$SMOKE_HOME" codex app-server --listen stdio://
```
- [ ] Expected `hooks/list` result:
- One Superpowers `SessionStart` plugin hook appears from `hooks/hooks-codex.json`.
- The hook matcher is `startup|resume|clear`.
- The expanded command points at `hooks/run-hook.cmd session-start`.
- Initial `trustStatus` is `untrusted` unless the temp config already contains a trusted hash.
- [ ] Trust the hook only inside the temp home using `hooks/list` `currentHash` plus `config/batchWrite`.
- [ ] Start a clean Codex thread and trigger a turn.
- [ ] Expected event/result:
- `hook/started` and `hook/completed` events are emitted for the Superpowers `SessionStart` hook.
- The resulting context includes the Superpowers startup text from `using-superpowers`.
- [ ] Keep this as a development smoke only. Do not document headless trust as normal user setup.
## Task 9: Run Codex App Visible UI Smoke
- [ ] Confirm the App-bundled Codex runtime version:
```bash
/Applications/Codex.app/Contents/Resources/codex --version
```
- [ ] With `plugin_hooks` enabled in persisted Codex config, launch the Codex App.
- [ ] Install or use the staged Superpowers plugin in the App.
- [ ] Open a clean App thread and observe the startup warning, if any.
- [ ] Open `/hooks`.
- [ ] Expected UI result:
- The Superpowers `SessionStart` hook is listed.
- The UI shows the hook command/source for review.
- Trusting the hook is possible from the UI.
- Toggling enabled after trust makes the hook runnable.
- [ ] Run the acceptance prompt in a clean App thread:
```text
Let's make a react todo list
```
- [ ] Passing App result:
- The agent sees the Superpowers startup context automatically.
- `superpowers:brainstorming` triggers before code is written.
- [ ] If the App UI smoke passes, update README App docs in Task 6 and record the transcript or concise evidence in the PR body.
- [ ] If the App UI smoke is blocked or fails, leave README App docs limited to plugin installation and report the exact blocker.
## Task 10: Final Verification
- [ ] Run:
```bash
bash tests/hooks/test-session-start.sh
bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
node -e 'for (const f of [".codex-plugin/plugin.json","hooks/hooks-codex.json","hooks/hooks.json","hooks/hooks-cursor.json"]) JSON.parse(require("fs").readFileSync(f,"utf8"));'
git diff --check
```
- [ ] Run the Codex CLI smoke from Task 8.
- [ ] Run the App UI smoke from Task 9, or record why it could not be completed.
- [ ] Review the diff:
```bash
git diff -- .codex-plugin/plugin.json hooks scripts tests README.md docs/windows/polyglot-hooks.md
```
- [ ] Confirm no unrelated files changed.
- [ ] If opening a PR, follow `.github/PULL_REQUEST_TEMPLATE.md`, search open and closed PRs for overlapping Codex hook work, include the relevant smoke evidence, and call out whether App UI smoke passed.
## Completion Criteria
- `hooks/hooks-codex.json` exists and is referenced by `.codex-plugin/plugin.json`.
- Codex plugin sync includes the `hooks/` directory and tests prevent future accidental exclusion.
- `hooks/session-start` still emits valid JSON for Claude, Codex, Cursor, Copilot, and unknown platform paths.
- Codex docs explain enablement, `/hooks` trust, re-review after updates, and fallback behavior without overclaiming default automatic startup.
- App docs claim automatic startup only if the visible App `/hooks` UI smoke passes.
- No new dependencies are introduced.
- Claude Code, Cursor, and Copilot behavior is not changed except for any neutral warning text that tests explicitly cover.

View File

@@ -1,338 +0,0 @@
# Codex Native Hooks: Claude Parity Bootstrap
**Date:** 2026-05-12
**Status:** Draft
**Ticket:** SUP-248
Use Codex native plugin hooks to give Codex users the same automatic Superpowers startup behavior Claude Code users already get, while keeping the current Codex plugin/skills path as a fallback during the `plugin_hooks` gated phase.
## Problem
Superpowers works best when the agent receives the `using-superpowers` bootstrap before it starts normal work. Claude Code gets that behavior through the Superpowers `SessionStart` hook: the hook injects the full `using-superpowers` skill content into session context, which causes agents to use skills automatically.
Codex currently installs Superpowers as a plugin and exposes the skills, but the Codex plugin package does not ship or declare the Superpowers startup hook. That means Codex users can install the skills, but they do not get the same reliable "you have Superpowers" startup bootstrap. They are still closer to the current workaround path: rely on the plugin/skills surface and prompt behavior instead of a trusted lifecycle hook.
Codex 0.130.0 now has the pieces needed for parity:
- `hooks` is stable and enabled by default.
- `plugins` is stable and enabled by default.
- `plugin_hooks` exists, is under development, and defaults to false.
- Installed plugins can bundle lifecycle hooks through manifest `hooks` entries or a default `hooks/hooks.json`.
- Codex provides a `/hooks` review/trust UI, with trusted hook hashes stored under `hooks.state`.
## Goals
1. Give Codex CLI users Claude Code parity for the Superpowers startup bootstrap.
2. Give Codex App users the same behavior when the app supports `plugin_hooks`.
3. Keep the current Codex plugin/skills support path working as a fallback.
4. Preserve Codex's explicit hook review and trust model.
5. Reuse the existing Superpowers hook implementation where practical.
## Non-Goals
- No `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, permission, or enforcement hooks in this phase.
- No silent auto-trust or installer-side trust writes to `~/.codex/config.toml`.
- No new product-specific behavior beyond the existing Superpowers bootstrap.
- No removal of the existing Codex install path while `plugin_hooks` remains under development.
- No broad plugin sync rewrite beyond what is needed to ship hooks.
## Current Claude Code Behavior
The Claude Code plugin currently ships:
- `.claude-plugin/plugin.json` and marketplace metadata.
- The 14 Superpowers skills under `skills/`.
- `hooks/hooks.json`, which declares one `SessionStart` hook matching `startup|clear|compact`.
- `hooks/run-hook.cmd`, a cross-platform wrapper for macOS, Linux, and Windows Git Bash.
- `hooks/session-start`, which reads `skills/using-superpowers/SKILL.md`, wraps it in the Superpowers bootstrap message, and emits `hookSpecificOutput.additionalContext` for Claude Code.
The Claude hook is bootstrap-only. It does not block tools, rewrite commands, or enforce policy. Its job is to make the agent aware of Superpowers at session start.
## Product Behavior
Codex users should get the same startup bootstrap after installing and trusting the hook:
1. User installs Superpowers from the Codex plugin marketplace.
2. User enables plugin hooks while the feature is gated:
- CLI: `codex features enable plugin_hooks`.
- App: use the same persisted feature configuration, or an App-native feature setting if one is verified.
3. User starts Codex.
4. Codex discovers the Superpowers plugin `SessionStart` hook.
5. Codex asks the user to review and trust it through `/hooks`.
6. Once trusted, new Codex sessions receive the `using-superpowers` bootstrap automatically.
While `plugin_hooks` is under development and default-off, this is an optional preview automatic startup path, not the default quickstart. If `plugin_hooks` is unavailable, disabled, or untrusted, Superpowers still works through the current Codex plugin and skills path: skills are installed and callable, but the automatic startup bootstrap does not run.
Do not document `codex app --enable plugin_hooks` as the App enablement path unless implementation re-verifies that it affects the App process. Codex source review indicates the `app` subcommand does not currently inherit root CLI `--enable` feature overrides, so persisted config is the safer documented path.
## Design
### 0. Contract Discovery Spike
Completed 2026-05-12 against:
- Official `openai/codex` source checkout `ac466c0` (`2026-05-12T16:50:45-07:00`).
- Installed CLI: `codex-cli 0.130.0`.
- App-bundled runtime: `/Applications/Codex.app/Contents/Resources/codex`, `codex-cli 0.130.0-alpha.5`.
Observed contract:
- Manifest `hooks: "./hooks/manifest-hooks.json"` resolves and appears in `hooks/list`.
- Omitted manifest `hooks` discovers default `hooks/hooks.json`.
- Manifest hook paths replace default discovery; a plugin with both `hooks: "./hooks/manifest-hooks.json"` and `hooks/hooks.json` listed only the manifest hook.
- Manifest hook paths must start with `./` and stay within the plugin root.
- Plugin hook commands expand `${PLUGIN_ROOT}` and `${CLAUDE_PLUGIN_ROOT}` to the plugin root, and `${PLUGIN_DATA}` and `${CLAUDE_PLUGIN_DATA}` to the plugin data root.
- Hook process environment includes the same plugin root/data variables.
- `SessionStart` source values are `startup`, `resume`, and `clear`.
- `hooks/list` reports plugin hook keys in the form `plugin-id:relative-hook-source:event:index:index`, with `currentHash` and `trustStatus`.
- Untrusted plugin hooks list as enabled but do not run until trusted.
- Writing `hooks.state.<key>.trusted_hash` through `config/batchWrite` changes `trustStatus` to `trusted`.
- Changing the hook command after trust changes `trustStatus` to `modified`.
- A trusted plugin `SessionStart` hook runs on the first turn, emits `hook/started` and `hook/completed`, and injects `hookSpecificOutput.additionalContext`.
- Modified plugin hooks do not run.
- The App-bundled app-server runtime lists the same plugin hooks and trust states as the CLI runtime.
Unproven:
- The desktop App's visible `/hooks` review/trust UI path was not manually exercised. App runtime support is present, but docs should not claim user-facing App parity until that UI smoke passes.
- Claude Code acceptance of a `startup|clear|resume|compact` superset matcher still needs verification before changing the shared Claude hook declaration.
### 1. Ship Hooks In The Codex Plugin
The Codex plugin package should include:
- `hooks/hooks.json`
- `hooks/session-start`
- `hooks/run-hook.cmd`
The sync script currently excludes `/hooks/` from the Codex plugin package. Remove that exclusion or replace it with a narrower rule if future hook assets need filtering.
The sync tests should assert that the generated Codex plugin contains all hook files, and should use fixture coverage that fails if future sync changes accidentally drop hooks from the Codex package or PR/update preview output.
### 2. Declare The Hook In The Codex Manifest
Update `.codex-plugin/plugin.json` to declare the bundled hook:
```json
{
"hooks": "./hooks/hooks.json"
}
```
Codex can also discover default `hooks/hooks.json` when the manifest omits `hooks`, but declaring it makes the contract explicit and easier to test.
Codex manifest hook rules to preserve in tests and implementation:
- `hooks` may be a manifest path string, a manifest path string array, an inline hook object, or an inline hook object array.
- Manifest hook paths must use plugin-relative `./...` paths.
- If manifest `hooks` is present, it replaces default `hooks/hooks.json` discovery; tests should not expect both to load.
### 3. Choose The Hook Declaration And Matcher
Claude Code currently uses `hooks/hooks.json` with a `SessionStart` matcher of `startup|clear|compact`.
Codex source review and local testing should treat Codex `SessionStart` source values as `startup`, `resume`, and `clear`. `compact` is not a Codex `SessionStart` source today. The implementation must cover resumed Codex sessions; otherwise a resumed session can miss the bootstrap.
Preferred outcome:
- Keep one shared `hooks/hooks.json` if both harnesses accept a superset matcher.
- Use matcher `startup|clear|resume|compact` in the shared declaration if verified safe for Claude Code and Codex.
- Use a command variable that both harnesses expand. Codex plugin hooks provide `PLUGIN_ROOT` and `CLAUDE_PLUGIN_ROOT`; they do not require or provide `CODEX_PLUGIN_ROOT`.
If a shared declaration is not safe, introduce `hooks/hooks-codex.json` and point `.codex-plugin/plugin.json` at it. The Codex-specific declaration should use `startup|resume|clear` and a Codex-verified command variable, preferably `PLUGIN_ROOT`.
Shared declaration candidate:
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|resume|compact",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
}
]
}
]
}
}
```
If Claude Code does not expand `PLUGIN_ROOT`, the shared declaration can continue using `CLAUDE_PLUGIN_ROOT` because Codex also provides that variable for plugin hooks. Do not design around `CODEX_PLUGIN_ROOT`.
### 4. Keep One Session-Start Script
`hooks/session-start` should remain the shared implementation. It already derives `PLUGIN_ROOT` from its own location, reads `skills/using-superpowers/SKILL.md`, escapes the bootstrap text for JSON, and emits the Claude-compatible nested output.
Update it only as needed to make Codex explicit:
- Simulate Codex with `PLUGIN_ROOT` and `CLAUDE_PLUGIN_ROOT`, not `CODEX_PLUGIN_ROOT`.
- Emit nested `hookSpecificOutput.additionalContext` for Codex `SessionStart`.
- Avoid the current unknown-platform top-level `additionalContext` fallback when running under Codex, because Codex's `SessionStart` parser expects `hookSpecificOutput.additionalContext`.
- Preserve existing Claude Code, Cursor, and Copilot behavior.
- Preserve Cursor's top-level `additional_context` behavior from `hooks/hooks-cursor.json`.
- Preserve Copilot's top-level `additionalContext` behavior.
- Make the legacy custom-skills warning platform-aware. The current warning tells users to move skills to `~/.claude/skills`; Codex users should not see Claude-specific migration advice unless it is also correct for their harness.
- Keep failure behavior quiet: if the script cannot read the skill file or a platform does not run hooks, basic plugin install still works.
The injected bootstrap content should match Claude Code parity: same `using-superpowers` content, same "You have superpowers" wrapper, and no Codex-specific policy beyond platform tool mappings already present in the skill references.
### 5. Preserve The Fallback Path
Do not remove or downplay current Codex plugin support. Update docs to describe two paths:
**Optional preview automatic startup path:** Codex 0.130.0+ with `plugin_hooks` enabled and the Superpowers hook trusted.
**Fallback path:** install the Superpowers plugin and use the available skills without native hook bootstrap. This remains necessary for users on older Codex builds, users who do not enable under-development features, and users who choose not to trust executable hooks.
## Documentation Changes
Update the README Codex CLI section:
- Keep the existing marketplace install steps.
- Add a short "Optional preview: enable automatic startup hook" subsection:
- `codex features enable plugin_hooks`
- install or update Superpowers
- start Codex
- open `/hooks`
- review and trust the Superpowers `SessionStart` hook
- Explain that the trust prompt is expected because hooks execute local commands.
- Say that if users skip hook enablement/trust, the plugin skills remain installed but the automatic startup bootstrap may not run.
- Say that after updating Superpowers, users may need to open `/hooks` and re-review the hook if Codex marks it modified since last trust.
Update the Codex App section only as far as evidence supports:
- If App smoke passes, document the same persisted feature configuration and `/hooks` review path.
- If App smoke does not pass, say App automatic startup hook parity is pending verified App support while normal plugin skills remain available.
Update any Codex plugin install docs or sync docs that describe the shipped file set so they include `hooks/`.
Update `docs/windows/polyglot-hooks.md` if it still describes older hook filenames. The current wrapper path is `hooks/run-hook.cmd` dispatching to extensionless `hooks/session-start`, and docs should match the implementation before Codex hook support ships.
## Safety And Trust
Superpowers should not bypass Codex's hook trust model.
The production install path must rely on Codex's own review UI. Headless trust through `hooks/list` plus `config/batchWrite` is useful for local development or smoke tests, but it should not be documented as a normal user install path and should not be baked into the plugin.
This is especially important because a plugin `SessionStart` hook runs a local command. Even though Superpowers only injects bootstrap context, the trust boundary belongs to Codex and the user.
## Verification Plan
### Automated
1. Contract spike: use the recorded discovery results above; re-run the spike if the target Codex version changes materially before implementation.
2. Sync script test: generated Codex plugin includes `hooks/hooks.json`, `hooks/session-start`, and `hooks/run-hook.cmd`.
3. Sync fixture test: sync previews and marketplace/update outputs mention the hook file set when relevant.
4. Manifest test: `.codex-plugin/plugin.json` declares the hook path and uses a valid `./...` manifest-relative path.
5. Hook JSON test: the Codex hook declaration remains valid JSON and contains exactly one `SessionStart` hook.
6. Hook matcher test: Codex declaration covers `startup`, `resume`, and `clear`; shared declaration may also include `compact` only if verified safe.
7. Hook output test: running `hooks/session-start` with a simulated Codex plugin environment emits valid JSON with `hookSpecificOutput.hookEventName = "SessionStart"` and non-empty `additionalContext`.
8. Regression test: simulated Claude Code environment still emits the existing Claude-compatible nested output.
9. Regression test: simulated Cursor environment still emits top-level `additional_context`.
10. Regression test: simulated Copilot environment still emits top-level `additionalContext`.
11. Warning test: when a legacy `~/.config/superpowers/skills` directory exists, Codex output does not show Claude-specific migration advice.
12. Documentation test or grep check: README Codex CLI section mentions `plugin_hooks`, `/hooks`, trust, update re-review, and fallback.
### Manual Codex CLI Smoke
1. Install or stage the Superpowers Codex plugin.
2. Enable plugin hooks: `codex features enable plugin_hooks`.
3. Start Codex 0.130.0+.
4. Open `/hooks`.
5. Confirm Superpowers `SessionStart` appears as a plugin hook needing review.
6. Trust the hook.
7. Start a fresh session.
8. Send exactly: `Let's make a react todo list`.
9. Confirm the agent auto-triggers `superpowers:brainstorming` before writing code.
10. Resume a session and confirm the bootstrap still runs or remains available under the `resume` source.
11. Clear a session and confirm the bootstrap still runs under the `clear` source.
### Manual Codex App Smoke
1. Enable plugin hooks with persisted Codex feature config: `codex features enable plugin_hooks`.
2. Install or stage the Superpowers plugin in the App-managed plugin surface.
3. Restart the Codex App.
4. Confirm the App exposes the hook review/trust path.
5. Trust the Superpowers `SessionStart` hook.
6. Start a fresh App session.
7. Send exactly: `Let's make a react todo list`.
8. Confirm the agent auto-triggers `superpowers:brainstorming` before writing code.
If the App cannot expose the hook review path yet, the implementation is still useful for CLI but the release notes and docs must say App hook parity is pending verified App support.
### Manual Windows/Path Smoke
1. Stage the plugin at a path containing spaces.
2. Run the hook through `hooks/run-hook.cmd` to verify the Windows/Git Bash dispatch chain still finds extensionless `hooks/session-start`.
3. Confirm no shell quoting regression from the hook command variable change.
### Marketplace Rollout Smoke
1. Bump plugin metadata version as required by the existing release flow.
2. Run the sync script and verify the Codex plugin output includes hooks and manifest hook metadata.
3. Verify any marketplace PR or publication artifact includes hooks.
4. After marketplace publication, run `codex plugin marketplace upgrade` or reinstall/update through the supported path.
5. Restart Codex.
6. Open `/hooks`; if the hook is untrusted or modified since last trust, review and trust it again.
7. Run the CLI acceptance transcript.
## Acceptance Criteria
- The Codex contract spike is recorded in this spec and summarized in the implementation notes or PR body.
- Codex plugin packages include the Superpowers hook files.
- Codex manifest declares the hook path.
- The final hook declaration covers Codex `startup`, `resume`, and `clear` sources.
- Codex CLI can discover, review, trust, and run the Superpowers `SessionStart` hook.
- Codex CLI acceptance transcript shows `Let's make a react todo list` auto-triggering `superpowers:brainstorming` before code is written.
- Codex App support is verified manually with the same acceptance transcript or explicitly marked as pending in docs.
- Claude Code hook behavior is unchanged.
- Cursor and Copilot hook output shapes are unchanged.
- Current Codex plugin/skills fallback remains documented and functional.
- README does not imply automatic startup for Codex users who have not enabled and trusted plugin hooks.
- No enforcement hooks are added.
- No normal user install path writes trusted hook hashes directly.
- Hook updates require the normal Codex re-review/re-trust path.
## Alternatives Considered
### Codex-Specific Hook Copy
Create `hooks/hooks-codex.json` and possibly `hooks/session-start-codex`.
This lowers the risk of breaking Claude Code, but it duplicates behavior and makes future drift likely. Use this only if Codex's source matcher or command variable contract makes a shared hook file impractical.
### Docs-Only Hook Guidance
Document how users can configure hooks manually without shipping hooks in the Codex plugin.
This does not solve the product problem. It keeps Codex on the workaround path instead of giving users the same automatic startup behavior Claude Code users get.
### Auto-Trust During Install
Use `hooks/list` and `config/batchWrite` to write `hooks.state.<key>.trusted_hash`.
This is technically possible for development automation, but it is the wrong product boundary. Trusting executable plugin hooks should remain an explicit Codex/user action.
## Open Risks
- Codex App may lag Codex CLI in hook review UI exposure even though the app bundle includes a 0.130.x Codex binary.
- Claude Code may not expand `PLUGIN_ROOT`, requiring the shared declaration to keep using `CLAUDE_PLUGIN_ROOT` or the Codex package to use a separate declaration.
- A shared matcher of `startup|clear|resume|compact` must be verified in both harnesses before release. If either harness rejects unknown sources, split the declaration files.
- `plugin_hooks` is under development and default-off, so docs must avoid implying this is stable-by-default behavior.
- App support should not be claimed until the App review/trust path and clean-session transcript are proven.
## Rollout
1. Use the recorded contract discovery results; re-run only if Codex changes materially before implementation.
2. Choose shared `hooks/hooks.json` or Codex-specific `hooks/hooks-codex.json` based on the recorded evidence plus Claude Code matcher verification.
3. Land hook packaging, tests, and README updates while keeping fallback instructions.
4. Verify Codex CLI with the exact acceptance transcript.
5. Verify Codex App, or mark App automatic startup as pending.
6. Publish/update the Codex plugin package through the existing marketplace flow.
7. Re-test from the marketplace-installed plugin and re-trust the hook if Codex marks it modified.
8. Revisit fallback wording once `plugin_hooks` becomes stable and default-enabled in Codex.

View File

@@ -1,107 +1,75 @@
# Cross-Platform Polyglot Hooks
# Cross-Platform Polyglot Hooks for Claude Code
Superpowers plugin hooks need to work on Windows, macOS, and Linux across the
agent harnesses that support startup hooks. This document explains the
polyglot wrapper technique that makes this possible.
Claude Code plugins need hooks that work on Windows, macOS, and Linux. This document explains the polyglot wrapper technique that makes this possible.
## The Problem
Hook commands may run through the system's default shell:
Claude Code runs hook commands through the system's default shell:
- **Windows**: CMD.exe
- **macOS/Linux**: bash or sh
This creates several challenges:
1. **Script execution**: Windows CMD can't execute shell scripts directly.
2. **Path format**: Windows uses backslashes (`C:\path`), Unix uses forward slashes (`/path`).
3. **Environment variables**: `$VAR` syntax doesn't work in CMD.
4. **No `bash` in PATH**: Even with Git Bash installed, `bash` isn't always in the PATH when CMD runs.
1. **Script execution**: Windows CMD can't execute `.sh` files directly - it tries to open them in a text editor
2. **Path format**: Windows uses backslashes (`C:\path`), Unix uses forward slashes (`/path`)
3. **Environment variables**: `$VAR` syntax doesn't work in CMD
4. **No `bash` in PATH**: Even with Git Bash installed, `bash` isn't in the PATH when CMD runs
## The Solution: Polyglot `run-hook.cmd` Wrapper
## The Solution: Polyglot `.cmd` Wrapper
A polyglot script is valid syntax in multiple languages simultaneously. Our
wrapper is valid in both CMD and bash. Manifests point to `run-hook.cmd` and
pass the extensionless hook script name:
A polyglot script is valid syntax in multiple languages simultaneously. Our wrapper is valid in both CMD and bash:
```cmd
: << 'CMDBLOCK'
@echo off
if "%~1"=="" (
echo run-hook.cmd: missing script name >&2
exit /b 1
)
set "HOOK_DIR=%~dp0"
if exist "C:\Program Files\Git\bin\bash.exe" (
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
where bash >nul 2>nul
if %ERRORLEVEL% equ 0 (
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
exit /b 0
"C:\Program Files\Git\bin\bash.exe" -l -c "\"$(cygpath -u \"$CLAUDE_PLUGIN_ROOT\")/hooks/session-start.sh\""
exit /b
CMDBLOCK
# Unix: run the named script directly
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
# Unix shell runs from here
"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
```
### How It Works
#### On Windows (CMD.exe)
1. `: << 'CMDBLOCK'` - CMD sees `:` as a label and ignores `<< 'CMDBLOCK'`.
2. `@echo off` - Suppresses command echoing.
3. The bash.exe command runs the requested hook script next to the wrapper.
4. `exit /b` - Exits the batch script, stopping CMD here.
5. Everything after `CMDBLOCK` is never reached by CMD.
1. `: << 'CMDBLOCK'` - CMD sees `:` as a label (like `:label`) and ignores `<< 'CMDBLOCK'`
2. `@echo off` - Suppresses command echoing
3. The bash.exe command runs with:
- `-l` (login shell) to get proper PATH with Unix utilities
- `cygpath -u` converts Windows path to Unix format (`C:\foo``/c/foo`)
4. `exit /b` - Exits the batch script, stopping CMD here
5. Everything after `CMDBLOCK` is never reached by CMD
#### On Unix (bash/sh)
1. `: << 'CMDBLOCK'` - `:` is a no-op, `<< 'CMDBLOCK'` starts a heredoc.
2. Everything until `CMDBLOCK` is consumed by the heredoc and ignored.
3. `# Unix shell runs from here` - Comment.
4. The requested hook script runs directly with the Unix path.
1. `: << 'CMDBLOCK'` - `:` is a no-op, `<< 'CMDBLOCK'` starts a heredoc
2. Everything until `CMDBLOCK` is consumed by the heredoc (ignored)
3. `# Unix shell runs from here` - Comment
4. The script runs directly with the Unix path
## File Structure
```text
```
hooks/
|-- hooks.json
|-- hooks-codex.json
|-- hooks-cursor.json
|-- run-hook.cmd
`-- session-start
├── hooks.json # Points to the .cmd wrapper
├── session-start.cmd # Polyglot wrapper (cross-platform entry point)
└── session-start.sh # Actual hook logic (bash script)
```
### `hooks/hooks.json` (Claude Code)
`hooks/hooks.json` is the Claude Code manifest:
### hooks.json
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"matcher": "startup|resume|clear|compact",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.cmd\""
}
]
}
@@ -110,79 +78,41 @@ hooks/
}
```
### `hooks/hooks-codex.json` (Codex)
`hooks/hooks-codex.json` is the Codex-specific manifest. Codex uses the
verified `${PLUGIN_ROOT}` placeholder and the `startup|resume|clear` matcher:
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
}
]
}
]
}
}
```
The matcher differs from Claude Code intentionally: the Codex spike verified
`startup`, `resume`, and `clear` as Codex `SessionStart` sources. Keep
Claude Code on `startup|clear|compact` until `resume` is explicitly verified
there, and keep Codex off `compact` until Codex support for that source is
verified.
Note: The path must be quoted because plugin roots may contain spaces on
Windows, for example `C:\Program Files\...`.
Note: The path must be quoted because `${CLAUDE_PLUGIN_ROOT}` may contain spaces on Windows (e.g., `C:\Program Files\...`).
## Requirements
### Windows
- **Git for Windows** must be installed if no other Bash is available.
- **Git for Windows** must be installed (provides `bash.exe` and `cygpath`)
- Default installation path: `C:\Program Files\Git\bin\bash.exe`
- If Git is installed elsewhere, `run-hook.cmd` also tries `bash` on PATH.
- If Git is installed elsewhere, the wrapper needs modification
### Unix (macOS/Linux)
- Standard bash or sh shell
- `run-hook.cmd` must have execute permission (`chmod +x`)
- The `.cmd` file must have execute permission (`chmod +x`)
## Writing Cross-Platform Hook Scripts
Your actual hook logic goes in the extensionless hook script. To ensure it
works on Windows via Git Bash:
Your actual hook logic goes in the `.sh` file. To ensure it works on Windows (via Git Bash):
### Do:
- Use pure bash builtins when possible
- Use `$(command)` instead of backticks
- Quote all variable expansions: `"$VAR"`
- Use `printf` or here-docs for output
### Avoid:
- External commands that may not be in PATH (sed, awk, grep)
- If you must use them, they're available in Git Bash but ensure PATH is set up
- If you must use them, they're available in Git Bash but ensure PATH is set up (use `bash -l`)
### Example: JSON Escaping Without sed/awk
Instead of:
```bash
escaped=$(echo "$content" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
```
Use pure bash:
```bash
escape_for_json() {
local input="$1"
@@ -205,48 +135,26 @@ escape_for_json() {
## Reusable Wrapper Pattern
For plugins with multiple hooks, use the generic wrapper with the script name
as an argument:
### `run-hook.cmd`
For plugins with multiple hooks, you can create a generic wrapper that takes the script name as an argument:
### run-hook.cmd
```cmd
: << 'CMDBLOCK'
@echo off
if "%~1"=="" (
echo run-hook.cmd: missing script name >&2
exit /b 1
)
set "HOOK_DIR=%~dp0"
if exist "C:\Program Files\Git\bin\bash.exe" (
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
where bash >nul 2>nul
if %ERRORLEVEL% equ 0 (
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
exit /b 0
set "SCRIPT_DIR=%~dp0"
set "SCRIPT_NAME=%~1"
"C:\Program Files\Git\bin\bash.exe" -l -c "cd \"$(cygpath -u \"%SCRIPT_DIR%\")\" && \"./%SCRIPT_NAME%\""
exit /b
CMDBLOCK
# Unix: run the named script directly
# Unix shell runs from here
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
"${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
```
### Manifest using the reusable wrapper
### hooks.json using the reusable wrapper
```json
{
"hooks": {
@@ -256,7 +164,7 @@ exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start"
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start.sh"
}
]
}
@@ -267,7 +175,7 @@ exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" validate-bash"
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" validate-bash.sh"
}
]
}
@@ -279,37 +187,26 @@ exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
## Troubleshooting
### "bash is not recognized"
CMD can't find bash. The wrapper checks common Git for Windows paths and then
tries `bash` on PATH. If Bash is installed elsewhere, update the path.
CMD can't find bash. The wrapper uses the full path `C:\Program Files\Git\bin\bash.exe`. If Git is installed elsewhere, update the path.
### "cygpath: command not found" or "dirname: command not found"
Bash isn't running in the environment you expected. Make sure the wrapper is
calling the intended Bash installation.
Bash isn't running as a login shell. Ensure `-l` flag is used.
### Path has weird `\/` in it
`${CLAUDE_PLUGIN_ROOT}` expanded to a Windows path ending with backslash, then
`/hooks/...` was appended. Route through `run-hook.cmd` so the Windows branch
uses the wrapper directory directly.
`${CLAUDE_PLUGIN_ROOT}` expanded to a Windows path ending with backslash, then `/hooks/...` was appended. Use `cygpath` to convert the entire path.
### Script opens in text editor instead of running
The manifest is pointing directly to the shell script. Point to `run-hook.cmd`
instead.
The hooks.json is pointing directly to the `.sh` file. Point to the `.cmd` wrapper instead.
### Works in terminal but not as hook
Claude Code may run hooks differently. Test by simulating the hook environment:
```powershell
$env:CLAUDE_PLUGIN_ROOT = "C:\path\to\plugin"
cmd /c "C:\path\to\plugin\hooks\run-hook.cmd session-start"
cmd /c "C:\path\to\plugin\hooks\session-start.cmd"
```
## Related Issues
- [anthropics/claude-code#9758](https://github.com/anthropics/claude-code/issues/9758) - shell scripts open in editor on Windows
- [anthropics/claude-code#9758](https://github.com/anthropics/claude-code/issues/9758) - .sh scripts open in editor on Windows
- [anthropics/claude-code#3417](https://github.com/anthropics/claude-code/issues/3417) - Hooks don't work on Windows
- [anthropics/claude-code#6023](https://github.com/anthropics/claude-code/issues/6023) - CLAUDE_PROJECT_DIR not found

View File

@@ -1,16 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
}
]
}
]
}
}

View File

@@ -7,23 +7,11 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Codex plugin hooks set both unprefixed PLUGIN_* vars and Claude-compatible
# CLAUDE_* vars. Only the unprefixed data var is Codex-specific enough to
# distinguish Codex from Claude Code.
is_codex_hook=0
if [ -n "${PLUGIN_DATA:-}" ]; then
is_codex_hook=1
fi
# Check if legacy skills directory exists and build warning
warning_message=""
legacy_skills_dir="${HOME}/.config/superpowers/skills"
if [ -d "$legacy_skills_dir" ]; then
if [ "$is_codex_hook" -eq 1 ]; then
warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER: WARNING: Superpowers now uses your coding agent's native skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to a skills location supported by your coding agent. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
else
warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
fi
warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
fi
# Read using-superpowers content

View File

@@ -70,6 +70,7 @@ EXCLUDES=(
"/commands/"
"/docs/"
"/evals/"
"/hooks/"
"/lib/"
"/scripts/"
"/tests/"
@@ -419,7 +420,7 @@ if [[ $BOOTSTRAP -eq 1 ]]; then
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\`, \`assets/\`, and \`hooks/\`.
Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\` and \`assets/\`.
Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
@@ -429,7 +430,7 @@ else
COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Copies the tracked plugin files from upstream, including the committed Codex manifest, assets, and hooks.
Copies the tracked plugin files from upstream, including the committed Codex manifest and assets.
Run via: \`scripts/sync-to-codex-plugin.sh\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA

View File

@@ -178,7 +178,6 @@ write_upstream_fixture() {
"$repo/.private-journal" \
"$repo/assets" \
"$repo/evals/drill" \
"$repo/hooks" \
"$repo/scripts" \
"$repo/skills/example"
@@ -219,36 +218,6 @@ EOF
printf 'png fixture\n' > "$repo/assets/app-icon.png"
printf 'eval harness fixture\n' > "$repo/evals/drill/README.md"
cat > "$repo/hooks/hooks-codex.json" <<'EOF'
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
}
]
}
]
}
}
EOF
cat > "$repo/hooks/session-start" <<'EOF'
#!/usr/bin/env sh
echo "session-start fixture"
EOF
cat > "$repo/hooks/run-hook.cmd" <<'EOF'
@echo off
echo run-hook fixture
EOF
chmod +x "$repo/hooks/session-start" "$repo/hooks/run-hook.cmd"
cat > "$repo/skills/example/SKILL.md" <<'EOF'
# Example Skill
@@ -267,9 +236,6 @@ EOF
assets/app-icon.png \
assets/superpowers-small.svg \
evals/drill/README.md \
hooks/hooks-codex.json \
hooks/run-hook.cmd \
hooks/session-start \
package.json \
scripts/sync-to-codex-plugin.sh \
skills/example/SKILL.md
@@ -327,7 +293,6 @@ write_synced_destination_fixture() {
"$repo/plugins/superpowers/.codex-plugin" \
"$repo/plugins/superpowers/.private-journal" \
"$repo/plugins/superpowers/assets" \
"$repo/plugins/superpowers/hooks" \
"$repo/plugins/superpowers/skills/example/agents" \
"$repo/plugins/superpowers/skills/example"
@@ -344,36 +309,6 @@ EOF
printf 'png fixture\n' > "$repo/plugins/superpowers/assets/app-icon.png"
cat > "$repo/plugins/superpowers/hooks/hooks-codex.json" <<'EOF'
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
}
]
}
]
}
}
EOF
cat > "$repo/plugins/superpowers/hooks/session-start" <<'EOF'
#!/usr/bin/env sh
echo "session-start fixture"
EOF
cat > "$repo/plugins/superpowers/hooks/run-hook.cmd" <<'EOF'
@echo off
echo run-hook fixture
EOF
chmod +x "$repo/plugins/superpowers/hooks/session-start" "$repo/plugins/superpowers/hooks/run-hook.cmd"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
@@ -392,9 +327,6 @@ EOF
plugins/superpowers/.codex-plugin/plugin.json \
plugins/superpowers/assets/app-icon.png \
plugins/superpowers/assets/superpowers-small.svg \
plugins/superpowers/hooks/hooks-codex.json \
plugins/superpowers/hooks/run-hook.cmd \
plugins/superpowers/hooks/session-start \
plugins/superpowers/skills/example/agents/openai.yaml \
plugins/superpowers/skills/example/SKILL.md \
plugins/superpowers/.private-journal/keep.txt
@@ -610,9 +542,6 @@ main() {
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset"
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset"
assert_contains "$preview_section" "hooks/hooks-codex.json" "Preview includes Codex hook manifest"
assert_contains "$preview_section" "hooks/session-start" "Preview includes session-start hook"
assert_contains "$preview_section" "hooks/run-hook.cmd" "Preview includes hook command wrapper"
assert_contains "$preview_section" ".private-journal/keep.txt" "Preview includes tracked ignored file"
assert_not_contains "$preview_section" ".private-journal/leak.txt" "Preview excludes ignored untracked file"
assert_not_contains "$preview_section" "ignored-cache/" "Preview excludes pure ignored directories"

View File

@@ -1,252 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start"
WRAPPER_UNDER_TEST="$REPO_ROOT/hooks/run-hook.cmd"
FAILURES=0
TEST_ROOT="$(mktemp -d)"
cleanup() {
rm -rf "$TEST_ROOT"
}
trap cleanup EXIT
pass() {
echo " [PASS] $1"
}
fail() {
echo " [FAIL] $1"
FAILURES=$((FAILURES + 1))
}
make_home() {
local name="$1"
local home="$TEST_ROOT/$name/home"
mkdir -p "$home"
printf '%s\n' "$home"
}
assert_command_output() {
local description="$1"
local shape="$2"
local contains="$3"
local not_contains="$4"
local home="$5"
shift 5
local output
if ! output="$(env -i PATH="${PATH:-}" HOME="$home" "$@" 2>&1)"; then
fail "$description"
echo " hook exited non-zero"
echo "$output" | sed 's/^/ /'
return
fi
if printf '%s' "$output" | \
EXPECT_SHAPE="$shape" \
EXPECT_CONTAINS="$contains" \
EXPECT_NOT_CONTAINS="$not_contains" \
node -e '
const fs = require("fs");
const input = fs.readFileSync(0, "utf8");
let payload;
try {
payload = JSON.parse(input);
} catch (error) {
console.error(`invalid JSON: ${error.message}`);
process.exit(1);
}
function hasOwn(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
}
function fail(message) {
console.error(message);
process.exit(1);
}
const shape = process.env.EXPECT_SHAPE;
let context;
if (shape === "nested") {
if (!hasOwn(payload, "hookSpecificOutput")) {
fail("missing hookSpecificOutput");
}
if (hasOwn(payload, "additional_context") || hasOwn(payload, "additionalContext")) {
fail("nested output also included a top-level context field");
}
const hookOutput = payload.hookSpecificOutput;
if (!hookOutput || typeof hookOutput !== "object" || Array.isArray(hookOutput)) {
fail("hookSpecificOutput is not an object");
}
if (hookOutput.hookEventName !== "SessionStart") {
fail(`unexpected hookEventName: ${hookOutput.hookEventName}`);
}
context = hookOutput.additionalContext;
} else if (shape === "cursor") {
if (hasOwn(payload, "hookSpecificOutput")) {
fail("cursor output included hookSpecificOutput");
}
if (!hasOwn(payload, "additional_context")) {
fail("cursor output missing additional_context");
}
if (hasOwn(payload, "additionalContext")) {
fail("cursor output included additionalContext");
}
context = payload.additional_context;
} else if (shape === "sdk") {
if (hasOwn(payload, "hookSpecificOutput")) {
fail("sdk output included hookSpecificOutput");
}
if (!hasOwn(payload, "additionalContext")) {
fail("sdk output missing additionalContext");
}
if (hasOwn(payload, "additional_context")) {
fail("sdk output included additional_context");
}
context = payload.additionalContext;
} else {
fail(`unknown expected shape: ${shape}`);
}
if (typeof context !== "string" || context.trim() === "") {
fail("injected context was empty");
}
const expectedText = process.env.EXPECT_CONTAINS || "";
if (expectedText && !context.includes(expectedText)) {
fail(`context did not contain expected text: ${expectedText}`);
}
const forbiddenTexts = (process.env.EXPECT_NOT_CONTAINS || "")
.split("\u001f")
.filter(Boolean);
for (const forbiddenText of forbiddenTexts) {
if (context.includes(forbiddenText)) {
fail(`context contained forbidden text: ${forbiddenText}`);
}
}
'; then
pass "$description"
else
fail "$description"
echo " output:"
echo "$output" | sed 's/^/ /'
fi
}
echo "SessionStart hook output tests"
claude_home="$(make_home claude-code)"
assert_command_output \
"Claude Code emits nested SessionStart additionalContext" \
"nested" \
"" \
"" \
"$claude_home" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
codex_home="$(make_home codex-plugin-hooks)"
codex_data="$TEST_ROOT/codex-plugin-hooks/data"
mkdir -p "$codex_data"
assert_command_output \
"Codex plugin hooks emit nested SessionStart additionalContext" \
"nested" \
"" \
"" \
"$codex_home" \
PLUGIN_DATA="$codex_data" \
CLAUDE_PLUGIN_DATA="$codex_data" \
PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
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 emits nested SessionStart additionalContext" \
"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
cursor_home="$(make_home cursor)"
assert_command_output \
"Cursor emits top-level additional_context only" \
"cursor" \
"" \
"" \
"$cursor_home" \
CURSOR_PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
copilot_home="$(make_home copilot-cli)"
assert_command_output \
"Copilot CLI emits top-level additionalContext only" \
"sdk" \
"" \
"" \
"$copilot_home" \
COPILOT_CLI=1 \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
claude_legacy_home="$(make_home claude-legacy-warning)"
mkdir -p "$claude_legacy_home/.config/superpowers/skills"
assert_command_output \
"Claude legacy warning points custom skills to ~/.claude/skills" \
"nested" \
"Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead." \
"" \
"$claude_legacy_home" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
claude_data_home="$(make_home claude-data-warning)"
claude_data="$TEST_ROOT/claude-data-warning/data"
mkdir -p "$claude_data_home/.config/superpowers/skills" "$claude_data"
assert_command_output \
"Claude with CLAUDE_PLUGIN_DATA still uses Claude legacy warning" \
"nested" \
"Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead." \
"" \
"$claude_data_home" \
CLAUDE_PLUGIN_DATA="$claude_data" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
codex_legacy_home="$(make_home codex-legacy-warning)"
codex_legacy_data="$TEST_ROOT/codex-legacy-warning/data"
mkdir -p "$codex_legacy_home/.config/superpowers/skills" "$codex_legacy_data"
assert_command_output \
"Codex legacy warning uses harness-neutral custom-skill wording" \
"nested" \
"WARNING: Superpowers now uses your coding agent's native skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to a skills location supported by your coding agent. To make this message go away, remove ~/.config/superpowers/skills" \
"~/.claude/skills"$'\037'"Claude Code's skills system" \
"$codex_legacy_home" \
PLUGIN_DATA="$codex_legacy_data" \
CLAUDE_PLUGIN_DATA="$codex_legacy_data" \
PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
if [[ "$FAILURES" -gt 0 ]]; then
echo "STATUS: FAILED ($FAILURES failure(s))"
exit 1
fi
echo "STATUS: PASSED"