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.5.4",
"version": "1.6.0",
"description": "Claude Code Plugin for Codex. Delegate code reviews, investigations, tracked tasks, and transcript transfers from inside Codex.",
"author": {
"name": "CBEPX",
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

## [Unreleased]

## v1.6.0

### Added

- Expose exact `lastProgressAt` and numeric `progressAgeMs` freshness telemetry for active jobs in JSON and Markdown status output without adding automatic stall detection or termination.
- Record the external Claude Code host on session markers so headless same-owner delegation can be rejected before creating recursive jobs.

### Changed

- Send Claude prompts through stdin instead of process arguments, including large Unicode review context, while keeping model and effort flags absent when their resolved values are empty.
- Make `--view-state on-terminal` the canonical foreground result-view contract; retain `on-success` as a warning alias and keep `defer` unchanged. Warn when the deprecated `--timeout-ms` alias is accepted, and require status timeout controls to accompany `--wait`.
- Document that status/result inspection may identity-check and reconcile owned orphan jobs, while healthy reads and ready `setup --check` runs leave managed files unchanged.

### Fixed

- Trust stderr authentication text only for non-zero Claude exits without a terminal event, preserve structured authentication and rate-limit classification precedence, and surface stdin delivery failures as tracked command failures.
- Skip stop-review execution when the current turn has no captured baseline, use only the available Codex question tool, and keep network escalation narrow and subordinate to the active sandbox policy.
- Serialize unread notification updates through same-status transitions, require post-signal liveness proof before clearing Claude child handles, clear stale worker identity metadata at the Windows unverifiable ceiling, and use the spawn-safe identity probe for tracked workers.
- Keep owning Codex session IDs out of synthetic Claude session fields while retaining legacy stored-job compatibility without rewriting state on disk.

## v1.5.4

### Added
Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ It follows the shape of [openai/codex-plugin-cc](https://github.com/openai/codex
Install the fork release from the CBEPX marketplace snapshot:

```bash
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.5.4
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.6.0
codex plugin add cc@cbepx
```

Expand All @@ -59,8 +59,8 @@ The optional `npx` helper can install this fork release and enable the required
```bash
CC_PLUGIN_CODEX_MARKETPLACE_NAME=cbepx \
CC_PLUGIN_CODEX_MARKETPLACE_SOURCE=CBEPX/cc-plugin-codex \
CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.5.4 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.5.4/cc-plugin-codex-1.5.4.tgz install
CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.6.0 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.6.0/cc-plugin-codex-1.6.0.tgz install
```

On Windows, prefer the marketplace path or the `npx` helper. The shell-script helper below is POSIX-only.
Expand Down Expand Up @@ -200,6 +200,8 @@ $cc:rescue --model sonnet --effort medium investigate the flaky test
| `--model <model>` | Claude model (`opus`, `sonnet`, `haiku`, `fable`, or full ID; defaults to `opus`). Aliases are resolved by Claude Code; a full ID pins a version. |
| `--effort <level>` | Reasoning effort: `low`, `medium`, `high`, `xhigh`, `max` (default: `xhigh` for opus, `high` for sonnet, unset for haiku and fable) |
| `--prompt-file <path>` | Read task description from a file |
| `--view-state on-terminal` | Mark the foreground terminal outcome as viewed |
| `--view-state defer` | Leave the terminal outcome unread for later inspection |
| `--wait-timeout-ms <ms>` | Foreground observer timeout before returning a retrievable job |
| `--timeout-ms <ms>` | Deprecated alias for `--wait-timeout-ms` |

Expand Down Expand Up @@ -278,6 +280,8 @@ All review and rescue commands support `--background`. Background jobs are track
5. **Session ownership** — jobs stay attached to the user-facing parent Codex session even when a built-in rescue/review child does the actual work, so plain `$cc:status`, `$cc:result`, and resume-candidate detection still follow the parent thread.
6. **Cleanup on exit** — when your Codex session ends, any still-running detached jobs are terminated via PID identity validation, and stale reserved job markers are cleaned up over time.

Job inspection can also reconcile stale `queued`, `running`, or `cancelling` records. It only terminates an orphaned owned process after its recorded PID identity matches; healthy active jobs are left unchanged. `$cc:result` and JSON status access may additionally record that a terminal result was viewed.

**Typical background flow:**

```text
Expand Down Expand Up @@ -306,6 +310,7 @@ The review gate is an **optional** stop-time hook. When enabled, pressing Ctrl+C
- **Token cost.** Every Ctrl+C triggers a Claude invocation. This can drain usage limits quickly if you stop often.
- **15-minute timeout.** The gate has a hard timeout. If Claude doesn't respond, the stop is allowed.
- **Skip-on-no-edits.** The gate computes a working-tree fingerprint baseline and skips review when the last Codex turn made no net edits.
- **Requires a recorded user turn.** If the UserPromptSubmit hook did not record a baseline for this session, the gate skips review instead of reviewing unrelated or headless work.
- **Not in nested sessions.** Child sessions (e.g., rescue subagents) suppress the gate to avoid feedback loops.

**Only enable when you're actively monitoring the session.**
Expand Down Expand Up @@ -336,7 +341,7 @@ The review gate is an **optional** stop-time hook. When enabled, pressing Ctrl+C
Install from the fork's marketplace snapshot:

```bash
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.5.4
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.6.0
codex plugin add cc@cbepx
```

Expand All @@ -357,8 +362,8 @@ This fork does not install from the upstream Sendbird marketplace. Use the CBEPX
```bash
CC_PLUGIN_CODEX_MARKETPLACE_NAME=cbepx \
CC_PLUGIN_CODEX_MARKETPLACE_SOURCE=CBEPX/cc-plugin-codex \
CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.5.4 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.5.4/cc-plugin-codex-1.5.4.tgz install
CC_PLUGIN_CODEX_MARKETPLACE_REF=v1.6.0 \
npx -y https://github.com/CBEPX/cc-plugin-codex/releases/download/v1.6.0/cc-plugin-codex-1.6.0.tgz install
```

After install, run:
Expand Down Expand Up @@ -388,7 +393,7 @@ $cc:setup
Re-run the fork marketplace install flow, pinned to the release you want:

```bash
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.5.4
codex plugin marketplace add CBEPX/cc-plugin-codex --ref v1.6.0
codex plugin add cc@cbepx
```

Expand Down
11 changes: 11 additions & 0 deletions hooks/lib/host-origin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Copyright 2026 Sendbird, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import process from "node:process";

export function detectExternalHostOrigin() {
return process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT
? "claude-code"
: null;
}
5 changes: 4 additions & 1 deletion hooks/session-lifecycle-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import process from "node:process";
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 { terminateProcessTreeIfIdentityMatches } from "../scripts/lib/process.mjs";
import {
Expand Down Expand Up @@ -341,7 +342,9 @@ function handleSessionStart(input) {
reportLifecycleFailure("SessionStart", error);
}
if (workspaceRoot) {
setCurrentSession(workspaceRoot, input.session_id);
setCurrentSession(workspaceRoot, input.session_id, {
hostOrigin: detectExternalHostOrigin(),
});
}
}
}
Expand Down
22 changes: 18 additions & 4 deletions hooks/stop-review-gate-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const SKIP_INTERACTIVE_HOOKS_ENV = "CLAUDE_COMPANION_SKIP_INTERACTIVE_HOOKS";
const STOP_REVIEW_SUCCESS_NOTE = "Claude Code stop-time review passed.";
const STOP_REVIEW_NO_EDIT_NOTE =
"Claude Code stop-time review skipped: the most recent turn made no net edits.";
const STOP_REVIEW_NO_BASELINE_NOTE =
"Claude Code stop-time review skipped: no user turn was recorded for this Codex session.";

function emitDecision(payload) {
process.stdout.write(`${JSON.stringify(payload)}\n`);
Expand Down Expand Up @@ -268,14 +270,26 @@ function evaluateTurnEditGate(cwd, workspaceRoot, sessionId) {
}

const baseline = readTurnBaseline(workspaceRoot, sessionId);
if (!baseline?.fingerprint) {
if (!baseline) {
return {
shouldSkipReview: false,
shouldSkipReview: true,
skipStatus: "skipped_no_turn_baseline",
skipNote: STOP_REVIEW_NO_BASELINE_NOTE,
reason: "No turn baseline was recorded for this session.",
baseline,
current: null,
};
}
if (!baseline.fingerprint) {
return {
shouldSkipReview: false,
reason: baseline.captureError
? `Turn-baseline capture failed: ${baseline.captureError}`
: "Turn baseline has no usable fingerprint.",
baseline,
current: null,
};
}

try {
const current = getWorkingTreeFingerprint(cwd);
Expand Down Expand Up @@ -372,13 +386,13 @@ async function main() {
};
if (turnEditGate.shouldSkipReview) {
persistFinal({
status: "skipped_no_turn_edits",
status: turnEditGate.skipStatus ?? "skipped_no_turn_edits",
reason: turnEditGate.reason,
claudeInvoked: false,
runningTaskNote,
...fingerprintFields,
});
logNote(STOP_REVIEW_NO_EDIT_NOTE);
logNote(turnEditGate.skipNote ?? STOP_REVIEW_NO_EDIT_NOTE);
logNote(runningTaskNote);
return;
}
Expand Down
39 changes: 33 additions & 6 deletions hooks/unread-result-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import path from "node:path";
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 {
getConfig,
getCurrentSessionMarker,
listJobs,
patchJob,
setCurrentSession,
TERMINAL_JOB_STATUSES,
transitionJob,
writeTurnBaseline,
} from "../scripts/lib/state.mjs";
import { getWorkingTreeFingerprint } from "../scripts/lib/git.mjs";
Expand Down Expand Up @@ -83,9 +86,13 @@ function selectUnreadTerminalJobs(workspaceRoot, sessionId) {
function markJobsNotified(workspaceRoot, jobs) {
const timestamp = nowIso();
for (const job of jobs) {
patchJob(workspaceRoot, job.id, {
notifiedAt: timestamp,
});
try {
transitionJob(workspaceRoot, job.id, [job.status], job.status, {
notifiedAt: timestamp,
});
} catch {
// Notification state is best-effort; still surface the terminal result.
}
}
}

Expand All @@ -101,8 +108,18 @@ function captureTurnBaseline(workspaceRoot, sessionId, cwd) {
capturedAt: nowIso(),
fingerprint,
});
} catch {
// Baseline capture is best-effort. If it fails, Stop falls back to running review.
} catch (error) {
try {
writeTurnBaseline(workspaceRoot, sessionId, {
cwd,
workspaceRoot,
capturedAt: nowIso(),
fingerprint: null,
captureError: error instanceof Error ? error.message : String(error),
});
} catch {
// Baseline capture is best-effort. A missing record also keeps Stop fail-open.
}
}
}

Expand All @@ -124,6 +141,16 @@ async function main() {
}

const config = getConfig(workspaceRoot);
try {
const currentSession = getCurrentSessionMarker(workspaceRoot);
if (!currentSession || currentSession.sessionId === sessionId) {
setCurrentSession(workspaceRoot, sessionId, {
hostOrigin: detectExternalHostOrigin(),
});
}
} catch {
// Best effort: an invalid session id must not fail a user prompt.
}
if (config.stopReviewGate) {
captureTurnBaseline(workspaceRoot, sessionId, cwd);
}
Expand Down
3 changes: 2 additions & 1 deletion internal-skills/cli-runtime/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Routing controls:
- Treat `--cwd`, `--model`, `--effort`, `--resume`, `--resume-last`, `--fresh`, `--prompt-file`, `--view-state`, `--owner-session-id`, and `--job-id` as routing controls, not task text.
- Preserve the parent helper's exact non-empty `workspaceRoot` as `--cwd "<workspaceRoot>"`; never substitute the plugin root or the child's default working directory.
- Leave `--model` and `--effort` unset unless the user explicitly asks for a specific model or effort. The companion command applies these defaults itself: model defaults to `opus`, effort defaults to `xhigh` for opus, `high` for sonnet, and is left unset for haiku and fable.
- `--view-state on-success` means the user will see this companion result in the current turn, so the companion may mark it viewed on success.
- `--view-state on-terminal` means the user will see this companion result in the current turn, so the companion may mark any terminal outcome viewed.
- `--view-state defer` means the parent is not waiting, so the companion must leave the result unread until the user explicitly checks it.
- `--owner-session-id <session-id>` is an internal parent-session routing control. Preserve it when present so tracked jobs remain visible to the parent session's `$cc:status` / `$cc:result`.
- Never emit an empty routing placeholder such as `--owner-session-id --job-id`.
Expand All @@ -47,6 +47,7 @@ Task defaults:
- If the tool output includes stderr progress chatter and a final stdout-style result, ignore the progress chatter and preserve only the final stdout-equivalent result text.
- Return the stdout of the `task` command exactly as-is.
- Run the companion command in the foreground; do not add shell backgrounding such as `&`, `nohup`, or detached `spawn`.
- Only when the shell tool schema and active policy permit escalation, pass `sandbox_permissions: "require_escalated"` with the justification `Allow the Claude Code companion to contact the Claude API for this requested task.` If a higher-priority policy forbids escalation, obey it and surface any network denial. Do not enable or change global network access, and do not first try the companion command in the default network-disabled sandbox.
- If the shell tool returns a session id, keep polling that same session until the companion command exits.
- Exit code 0 is the only successful completion.
- Exit code 124 means the job is still running; return the companion output without claiming it finished.
Expand Down
6 changes: 4 additions & 2 deletions internal-skills/review-runtime/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ Foreground contract:
- Strip `--wait` and `--background` before building the companion command.
- Keep the shell tool in the active user workspace; do not set its working directory to the plugin path.
- Foreground command:
- `review --view-state on-success ...`
- `adversarial-review --view-state on-success ...`
- `review --view-state on-terminal ...`
- `adversarial-review --view-state on-terminal ...`
- Only when the shell tool schema and active policy permit escalation, pass `sandbox_permissions: "require_escalated"` with the justification `Allow the Claude Code companion to contact the Claude API for this requested review.` If a higher-priority policy forbids escalation, obey it and surface any network denial. Do not enable or change global network access, and do not first try the companion command in the default network-disabled sandbox.
- Return companion stdout faithfully and do not add review execution commentary around it.

Background contract:
Expand All @@ -37,6 +38,7 @@ Background contract:
- ignore stderr progress chatter such as `[cc] ...`
- do not inspect the repo or perform the review itself
- run the companion command in the foreground; do not add shell backgrounding such as `&`, `nohup`, or detached `spawn`
- only when the shell tool schema and active policy permit escalation, pass `sandbox_permissions: "require_escalated"` with the justification `Allow the Claude Code companion to contact the Claude API for this requested review.` If a higher-priority policy forbids escalation, obey it and surface any network denial. Do not enable or change global network access, and do not first try the companion command in the default network-disabled sandbox
- If the shell tool returns a session id, keep polling that same session until the companion command exits.
- Exit code 0 is the only successful completion.
- Exit code 124 means the job is still running; return the companion output without claiming it finished.
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.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cc-plugin-codex",
"version": "1.5.4",
"version": "1.6.0",
"description": "Claude Code Plugin for Codex (CBEPX fork)",
"type": "module",
"author": {
Expand Down Expand Up @@ -61,7 +61,7 @@
"test": "node --import ./tests/test-env.mjs --test tests/*.test.mjs",
"test:coverage": "c8 --all --include='scripts/**/*.mjs' --include='hooks/**/*.mjs' --reporter=text --reporter=json-summary --reporter=lcov --reports-dir=reports/coverage --check-coverage --lines=89 --statements=89 --branches=79 --functions=96 node --import ./tests/test-env.mjs --test tests/*.test.mjs tests/integration/*.test.mjs tests/e2e/*.test.mjs",
"test:cross-platform": "node --import ./tests/test-env.mjs --test tests/args.test.mjs tests/cancel-command.test.mjs tests/changelog.test.mjs tests/claude-cli.test.mjs tests/fs.test.mjs tests/install-hooks.test.mjs tests/mutation-config.test.mjs tests/plugin-install-guard.test.mjs tests/process.test.mjs tests/prompts.test.mjs tests/render.test.mjs tests/sandbox-modes.test.mjs tests/skills-contracts.test.mjs tests/structured-output.test.mjs tests/version-sync.test.mjs",
"test:lifecycle-contract": "node --import ./tests/test-env.mjs --test --test-name-pattern=\"keeps a running job while its owning worker is alive|falls back to the identity-checked Claude PID|terminates a live Claude child|bounds Windows Claude child cleanup|clears a recycled Claude child PID|clears an identity-unavailable Claude child|reports cancel_failed when a live Claude child|tracks the worker separately|logs when worker identity is unavailable|does not bypass a terminal writer|ignores progress after its job file disappears\" tests/state.test.mjs tests/tracked-jobs.test.mjs",
"test:lifecycle-contract": "node --import ./tests/test-env.mjs --test --test-name-pattern=\"keeps a running job while its owning worker is alive|falls back to the identity-checked Claude PID|terminates a live Claude child|waits briefly for a signalled Claude child|bounds Windows Claude child cleanup|clears a recycled Claude child PID|clears an identity-unavailable Claude child|reports cancel_failed when a live Claude child|tracks the worker separately|logs when worker identity is unavailable|does not bypass a terminal writer|ignores progress after its job file disappears\" tests/state.test.mjs tests/tracked-jobs.test.mjs",
"test:integration": "node --import ./tests/test-env.mjs --test tests/integration/*.test.mjs",
"test:mutation": "npm run test:mutation:pr",
"test:mutation:pr": "npm run test:mutation:critical && npm run test:mutation:shard:managed && npm run test:mutation:shard:installer",
Expand Down
Loading
Loading