Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cc",
"version": "1.4.2",
"version": "1.5.0",
"description": "Claude Code Plugin for Codex. Delegate code reviews, investigations, and tracked tasks to Claude Code from inside Codex.",
"author": {
"name": "Sendbird, Inc.",
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## v1.5.0

- Restore the `SessionEnd` hook. Codex 0.146 dispatches `SessionEnd` for root sessions, so the teardown removed in v1.3.0 as non-dispatched now runs again: it reaps background jobs whose process died and clears this session's current-session marker instead of leaving it to age out after seven days. Teardown stays inside Codex's few-second budget — it never kills or waits on live processes, so detached jobs keep running and the `UserPromptSubmit` sweeper still covers them. Codex trusts hooks one by one, so the new `SessionEnd` hook arrives untrusted and stays inert until you approve it in the Codex hooks browser; the already-trusted `SessionStart`, `Stop`, and `UserPromptSubmit` entries are unaffected, because both the trust key index and the hash are per hook.
- Stop requiring `[features].plugin_hooks`. Upstream retired that flag (`Stage::Removed`), and native plugin hooks now ride on `[features].hooks` alone. Setup requires only `hooks = true` and strips a leftover `plugin_hooks` line from `~/.codex/config.toml`, the same way it already upgrades the legacy `codex_hooks` alias.
- Correct the review skills' description of `request_user_input`. Omitting `[tools] experimental_request_user_input` leaves the tool enabled; only an explicit `false` hides it in an interactive thread. The conditional ask is unchanged — it still depends on the thread actually having a question tool.

## v1.4.2

- Refuse companion delegation from Codex threads that are themselves driven by Claude Code. The reverse-direction plugin (Claude Code → Codex) spawns a bare `codex app-server` that inherits `~/.codex`, so its headless review threads see this plugin's skills and delegated the review back to Claude Code — looping the work between the two assistants and burning minutes on `wait`-tool spins with narration in place of findings. Session hooks now stamp `hostOrigin: "claude-code"` on the current-session marker when Claude Code host env markers (`CLAUDECODE` / `CLAUDE_CODE_ENTRYPOINT`) reach them, and `review`, `adversarial-review`, and `task` refuse delegation from such threads with explicit instructions to perform the work directly in that thread. Interactive sessions (env session id present), background forwarding children owned by a different session, and unstamped state all stay open, so the gate fails open everywhere the loop cannot occur.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ $cc:setup --disable-review-gate # turn it off
```

Setup checks Claude Code availability, native plugin hook feature gates, and review-gate state. If Claude Code isn't installed, it offers to install it.
This is also the repair path for marketplace-installed copies of the plugin: `$cc:setup` confirms `[features].hooks = true` and `[features].plugin_hooks = true`, trusts this plugin's current native hook hashes, and allows sandboxed writes to Codex's injected marketplace-qualified plugin-data root plus the legacy roots needed for one-time migration. If those writable roots were just added, restart Codex and rerun setup before changing the review gate.
This is also the repair path for marketplace-installed copies of the plugin: `$cc:setup` confirms `[features].hooks = true`, trusts this plugin's current native hook hashes, and allows sandboxed writes to Codex's injected marketplace-qualified plugin-data root plus the legacy roots needed for one-time migration. If those writable roots were just added, restart Codex and rerun setup before changing the review gate.

## Background Jobs

Expand Down Expand Up @@ -308,7 +308,7 @@ Then install `cc` from the Sendbird marketplace inside Codex, and run:
$cc:setup
```

Marketplace/plugin install places the plugin under Codex's plugin cache. `$cc:setup` verifies Claude Code, confirms `[features].hooks = true` plus `[features].plugin_hooks = true`, and trusts the current `hooks/hooks.json` hook hashes from the active plugin cache.
Marketplace/plugin install places the plugin under Codex's plugin cache. `$cc:setup` verifies Claude Code, confirms `[features].hooks = true`, and trusts the current `hooks/hooks.json` hook hashes from the active plugin cache.

### npx helper

Expand Down Expand Up @@ -362,7 +362,7 @@ claude auth login
Re-run install and restart Codex. This plugin expects Codex plugin support and no longer installs local skill-wrapper fallbacks.

**Hooks not firing**
Check that `hooks = true` and `plugin_hooks = true` are set in `~/.codex/config.toml` under `[features]`. Run `$cc:setup` to verify and auto-repair the feature gates plus this plugin's hook trust hashes, then restart Codex if those flags were just changed.
Check that `hooks = true` is set in `~/.codex/config.toml` under `[features]`. Run `$cc:setup` to verify and auto-repair the feature gate plus this plugin's hook trust hashes, then restart Codex if that flag was just changed.

**A background job finished but I did not get the result nudge**
Use:
Expand Down
13 changes: 13 additions & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@
]
}
],
"SessionEnd": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node \"$PLUGIN_ROOT/hooks/session-lifecycle-hook.mjs\"",
"timeout": 3,
"statusMessage": "Closing Claude Code bridge"
}
]
}
],
"Stop": [
{
"hooks": [
Expand Down
27 changes: 26 additions & 1 deletion hooks/session-lifecycle-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Session lifecycle hook for Codex — Claude Code bridge.
*
* SessionStart: Exports CLAUDE_COMPANION_SESSION_ID via CLAUDE_ENV_FILE.
* SessionEnd: Reaps dead background jobs and drops the session marker.
*
* No broker lifecycle — Claude Code uses direct CLI invocation.
*/
Expand All @@ -21,7 +22,11 @@ import { fileURLToPath } from "node:url";
import { readHookInput } from "./lib/hook-input.mjs";
import { detectExternalHostOrigin } from "./lib/host-origin.mjs";
import { cleanupAfterOfficialUninstall } from "./lib/plugin-install-guard.mjs";
import { setCurrentSession } from "../scripts/lib/state.mjs";
import {
clearCurrentSession,
listJobs,
setCurrentSession,
} from "../scripts/lib/state.mjs";
import { SESSION_ID_ENV } from "../scripts/lib/tracked-jobs.mjs";

export { SESSION_ID_ENV };
Expand Down Expand Up @@ -68,6 +73,22 @@ function handleSessionStart(input) {
}
}

function handleSessionEnd(input) {
const cwd = input.cwd || process.cwd();
if (isNestedCodexSession(input.session_id)) {
return;
}
try {
// listJobs() runs the PID-reuse-safe stale job reaper. Codex caps SessionEnd
// at a few seconds, so teardown never kills or waits on live processes:
// detached jobs keep running and the UserPromptSubmit sweeper picks them up.
listJobs(cwd);
} catch {
// Best effort only — teardown must not fail the session shutdown.
}
clearCurrentSession(cwd, input.session_id ?? null);
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
Expand All @@ -79,6 +100,10 @@ async function main() {
}
const eventName = process.argv[2] ?? input.hook_event_name ?? "";

if (eventName === "SessionEnd") {
handleSessionEnd(input);
return;
}
if (eventName === "SessionStart" || !eventName) {
// Default to SessionStart (Codex invokes this on session start)
handleSessionStart(input);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cc-plugin-codex",
"version": "1.4.2",
"version": "1.5.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Synchronize the lockfile with the release version

This release bumps package.json to 1.5.0, but package-lock.json still declares 1.4.2 in both its top-level version and packages[""] entry. As a result, a standard npm install --package-lock-only immediately rewrites the tracked lockfile, leaving release metadata inconsistent; update those lockfile entries as part of the version bump.

Useful? React with 👍 / 👎.

"description": "Claude Code Plugin for Codex by Sendbird",
"type": "module",
"author": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/claude-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ async function handleSetup(argv) {

if (configureNativePluginHooks()) {
actionsTaken.push(
"Enabled native Codex plugin hooks via [features].hooks and [features].plugin_hooks."
"Enabled native Codex plugin hooks via [features].hooks."
);
actionsTaken.push("Restart Codex if this session started before the feature change.");
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/installer-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ async function installOrUpdate() {

console.log(`Installed ${PLUGIN_NAME} from ${marketplaceConfig.source} into the Codex plugin cache.`);
if (hooksChanged) {
console.log("Enabled [features].hooks and [features].plugin_hooks in ~/.codex/config.toml.");
console.log("Enabled [features].hooks in ~/.codex/config.toml.");
}
if (writableRootChanged) {
console.log(`Allowed plugin state writes under ${pluginDataRoots.join(", ")}.`);
Expand Down
7 changes: 5 additions & 2 deletions scripts/lib/codex-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ function normalizeTrailingNewline(text) {
return `${String(text).replace(/\s*$/, "")}\n`;
}

const REQUIRED_NATIVE_HOOK_FEATURES = ["hooks", "plugin_hooks"];
const REQUIRED_NATIVE_HOOK_FEATURES = ["hooks"];
// Upstream Codex retired these feature flags; `codex_hooks` became `hooks` and
// `plugin_hooks` folded into it. Leaving them set keeps dead keys in user config.
const OBSOLETE_NATIVE_HOOK_FEATURES = ["codex_hooks", "plugin_hooks"];
const WRITABLE_ROOTS_KEY = "sandbox_workspace_write.writable_roots";
const MAX_CONFIG_WRITE_ATTEMPTS = 3;

Expand Down Expand Up @@ -220,7 +223,7 @@ export function ensureNativePluginHooksEnabled(content) {
if (inFeatures) {
const featureMatch = trimmed.match(/^([A-Za-z0-9_.-]+)\s*=/);
const featureKey = featureMatch?.[1] ?? null;
if (featureKey === "codex_hooks") {
if (OBSOLETE_NATIVE_HOOK_FEATURES.includes(featureKey)) {
changed = true;
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion skills/adversarial-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Execution mode rules:
- Then ask the user once which execution mode to use, offering two options with the recommended one first and its label suffixed `(Recommended)`:
- `Wait for results`
- `Run in background`
- Use a question tool for that ask only when this thread actually has one. Codex exposes `request_user_input` only behind `[tools] experimental_request_user_input`, and it does not exist in non-interactive threads. If you have no question tool but a user is reading this thread, ask in your own reply and stop there. In a non-interactive thread with no user to answer, skip the ask and proceed with the recommended mode. Never spin on a wait or collaboration tool looking for a picker this thread does not have.
- Use a question tool for that ask only when this thread actually has one. Codex exposes `request_user_input` by default in interactive threads and hides it only when `[tools] experimental_request_user_input = false`, and it does not exist in non-interactive threads. If you have no question tool but a user is reading this thread, ask in your own reply and stop there. In a non-interactive thread with no user to answer, skip the ask and proceed with the recommended mode. Never spin on a wait or collaboration tool looking for a picker this thread does not have.

Argument handling:
- Preserve the user's arguments exactly.
Expand Down
2 changes: 1 addition & 1 deletion skills/review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Execution mode rules:
- Then ask the user once which execution mode to use, offering two options with the recommended one first and its label suffixed `(Recommended)`:
- `Wait for results`
- `Run in background`
- Use a question tool for that ask only when this thread actually has one. Codex exposes `request_user_input` only behind `[tools] experimental_request_user_input`, and it does not exist in non-interactive threads. If you have no question tool but a user is reading this thread, ask in your own reply and stop there. In a non-interactive thread with no user to answer, skip the ask and proceed with the recommended mode. Never spin on a wait or collaboration tool looking for a picker this thread does not have.
- Use a question tool for that ask only when this thread actually has one. Codex exposes `request_user_input` by default in interactive threads and hides it only when `[tools] experimental_request_user_input = false`, and it does not exist in non-interactive threads. If you have no question tool but a user is reading this thread, ask in your own reply and stop there. In a non-interactive thread with no user to answer, skip the ask and proceed with the recommended mode. Never spin on a wait or collaboration tool looking for a picker this thread does not have.

Argument handling:
- Preserve the user's arguments exactly.
Expand Down
2 changes: 1 addition & 1 deletion skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Workflow:
- If it reports that Claude Code is unavailable and `npm` is available, ask whether to install Claude Code now.
- If the user agrees, run `npm install -g @anthropic-ai/claude-code` and rerun setup.
- If Claude Code is already installed or `npm` is unavailable, do not ask about installation.
- If setup reports missing native plugin hook features or hook trust, rerun setup once. The companion repairs `[features].hooks`, `[features].plugin_hooks`, and this plugin's native hook trust hashes itself.
- If setup reports missing native plugin hook features or hook trust, rerun setup once. The companion repairs `[features].hooks` and this plugin's native hook trust hashes itself.
- If setup adds the plugin-data destination or legacy migration roots to the writable-root list, do not retry in the same Codex session. Tell the user to restart Codex and rerun the same setup command; any requested review-gate change is deliberately deferred until that restart.
- After the decision flow is complete, run the final user-facing command without `--json`:
`node "<plugin-root>/scripts/claude-companion.mjs" setup $ARGUMENTS`
Expand Down
31 changes: 30 additions & 1 deletion tests/codex-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { ensureCodexWritableRoot } from "../scripts/lib/codex-config.mjs";
import {
ensureCodexWritableRoot,
ensureNativePluginHooksEnabled,
nativePluginHooksStatus,
} from "../scripts/lib/codex-config.mjs";

const originalExecutable = process.env.CC_PLUGIN_CODEX_EXECUTABLE;
const originalArgs = process.env.CC_PLUGIN_CODEX_APP_SERVER_ARGS_JSON;
Expand Down Expand Up @@ -156,3 +160,28 @@ rl.on("line", (line) => {
]);
assert.equal(await ensureCodexWritableRoot(root, "/target"), false);
});

it("requires only [features].hooks and strips the retired plugin_hooks gate", () => {
const enabled = ensureNativePluginHooksEnabled(
"[features]\nhooks = true\nplugin_hooks = true\n"
);

assert.equal(enabled.changed, true);
assert.match(enabled.content, /hooks = true/);
assert.doesNotMatch(enabled.content, /plugin_hooks/);
assert.equal(nativePluginHooksStatus(enabled.content).installed, true);

const clean = ensureNativePluginHooksEnabled(enabled.content);
assert.equal(clean.changed, false);
});

it("reports native hook status from [features].hooks alone", () => {
assert.deepEqual(nativePluginHooksStatus("[features]\nhooks = true\n"), {
installed: true,
missing: [],
});
assert.deepEqual(nativePluginHooksStatus("[features]\nplugin_hooks = true\n"), {
installed: false,
missing: ["hooks"],
});
});
Loading