oh-my-claudecode
Splits a complex task across several specialized Claude Code agents working in parallel, then keeps going until the result is checked. You describe what you want in plain language instead of memorizing commands.
What cc-plugin-codex runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Claude Code
$ npx -y skills add sendbird/cc-plugin-codex --agent claude-code
Cursor
Cursor also runs Claude Code hooks from .claude/settings.json, so copy these hooks there to use them in Cursor.
What fires, and when
SessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
node "$PLUGIN_ROOT/hooks/session-lifecycle-hook.mjs"SessionEnd
node "$PLUGIN_ROOT/hooks/session-lifecycle-hook.mjs"Stop
node "$PLUGIN_ROOT/hooks/stop-review-gate-hook.mjs"UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
node "$PLUGIN_ROOT/hooks/unread-result-hook.mjs"Where it lives
#!/usr/bin/env node
/**
* Copyright 2026 Sendbird, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* 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.
*/
import fs from "node:fs";
import path from "node:path";
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 {
clearCurrentSession,
listJobs,
setCurrentSession,
} from "../scripts/lib/state.mjs";
import { SESSION_ID_ENV } from "../scripts/lib/tracked-jobs.mjs";
export { SESSION_ID_ENV };
const PLUGIN_DATA_ENV = "CLAUDE_PLUGIN_DATA";
const SKIP_INTERACTIVE_HOOKS_ENV = "CLAUDE_COMPANION_SKIP_INTERACTIVE_HOOKS";
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
function shellEscape(value) {
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
}
function appendEnvVar(name, value) {
if (!process.env.CLAUDE_ENV_FILE || value == null || value === "") {
return;
}
fs.appendFileSync(
process.env.CLAUDE_ENV_FILE,
`export ${name}=${shellEscape(value)}\n`,
"utf8"
);
}
function isNestedCodexSession(inputSessionId) {
const inheritedSessionId = process.env[SESSION_ID_ENV] || null;
return Boolean(
inputSessionId &&
inheritedSessionId &&
inheritedSessionId !== inputSessionId
);
}
function handleSessionStart(input) {
const cwd = input.cwd || process.cwd();
const nestedSession = isNestedCodexSession(input.session_id);
// Export session ID so companion scripts can correlate jobs
appendEnvVar(SESSION_ID_ENV, input.session_id);
appendEnvVar(SKIP_INTERACTIVE_HOOKS_ENV, nestedSession ? "1" : "0");
// Forward plugin data dir if set
appendEnvVar(PLUGIN_DATA_ENV, process.env[PLUGIN_DATA_ENV]);
if (input.session_id && !nestedSession) {
setCurrentSession(cwd, input.session_id, {
hostOrigin: detectExternalHostOrigin(),
});
}
}
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
// ---------------------------------------------------------------------------
async function main() {
const input = readHookInput();
if (cleanupAfterOfficialUninstall(ROOT_DIR)) {
return;
}
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);
}
}
main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
);
process.exit(1);
});
#!/usr/bin/env node
/**
* Copyright 2026 Sendbird, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Turn-end review gate hook for Codex — Claude Code bridge.
*
* Flow:
* 1. Check config.stopReviewGate — if disabled -> exit 0.
* 2. If Claude Code is not ready, log setup guidance and allow stop to continue.
* 3. Run a targeted turn-end review of the previous Codex response.
* 4. Parse ALLOW:/BLOCK: from Claude's output.
* 5. If the review returns BLOCK, keep the Codex turn active.
*/
import process from "node:process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { readHookInput } from "./lib/hook-input.mjs";
import { cleanupAfterOfficialUninstall } from "./lib/plugin-install-guard.mjs";
import { loadPromptTemplate, interpolateTemplate } from "../scripts/lib/prompts.mjs";
import {
appendStopReviewHistory,
generateJobId,
getCurrentSession,
getConfig,
listJobs,
nowIso,
readTurnBaseline,
writeStopReviewSnapshot
} from "../scripts/lib/state.mjs";
import {
getClaudeAvailability,
getClaudeAuthStatus,
cleanupReviewMcpConfig,
cleanupSandboxSettings,
createReviewMcpConfig,
createSandboxSettings,
runClaudeReview,
SANDBOX_STOP_REVIEW_TOOLS,
} from "../scripts/lib/claude-cli.mjs";
import { getWorkingTreeFingerprint } from "../scripts/lib/git.mjs";
import { SESSION_ID_ENV } from "../scripts/lib/tracked-jobs.mjs";
import { resolveWorkspaceRoot } from "../scripts/lib/workspace.mjs";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..");
const SKIP_INTERACTIVE_HOOKS_ENV = "CLAUDE_COMPANION_SKIP_INTERACTIVE_HOOKS";
const STOP_REVIEW_SUCCESS_NOTE = "Claude Code turn-end review passed.";
const STOP_REVIEW_NO_EDIT_NOTE =
"Claude Code turn-end review skipped: the most recent turn made no net edits.";
const STOP_REVIEW_NO_BASELINE_NOTE =
"Claude Code turn-end review skipped: no user turn was recorded for this Codex session.";
const MAX_INLINE_REASON_CHARS = 1_500;
function emitDecision(payload) {
process.stdout.write(`${JSON.stringify(payload)}\n`);
}
function logNote(message) {
if (!message) {
return;
}
process.stderr.write(`${message}\n`);
}
function boundReasonForHookOutput(reason, runId) {
const text = String(reason ?? "");
if (text.length <= MAX_INLINE_REASON_CHARS) {
return text;
}
const suffix = [
"",
"",
`Full stop-review output was saved in the ${runId} snapshot.`
].join("\n");
return `${text.slice(0, MAX_INLINE_REASON_CHARS).trimEnd()}…${suffix}`;
}
function buildSetupNote(cwd) {
const availability = getClaudeAvailability(cwd);
if (!availability.available) {
return `Claude Code is not set up for the review gate. ${availability.detail}. Run $cc:setup.`;
}
const authStatus = getClaudeAuthStatus(cwd);
if (!authStatus.loggedIn) {
const detail = authStatus.detail ? ` ${authStatus.detail}.` : "";
return `Claude Code is not set up for the review gate.${detail} Run $cc:setup and, if needed, \`claude auth login\`.`;
}
return null;
}
// ---------------------------------------------------------------------------
// Prompt building
// ---------------------------------------------------------------------------
function buildStopReviewPrompt(input = {}) {
const lastAssistantMessage = String(input.last_assistant_message ?? "").trim();
const template = loadPromptTemplate(ROOT_DIR, "stop-review-gate");
const previousResponseBlock = lastAssistantMessage
? [
"<previous_codex_response>",
lastAssistantMessage,
"</previous_codex_response>",
].join("\n")
: "";
return interpolateTemplate(template, {
PREVIOUS_RESPONSE_BLOCK: previousResponseBlock
});
}
// ---------------------------------------------------------------------------
// Output parsing
// ---------------------------------------------------------------------------
function parseStopReviewOutput(rawOutput) {
const text = String(rawOutput ?? "").trim();
if (!text) {
return {
ok: false,
rawOutput: text,
firstLine: "",
reason:
"The turn-end Claude Code review returned no output. Run $cc:review --wait manually or bypass the gate."
};
}
const firstLine = text.split(/\r?\n/, 1)[0].trim();
const allowIndex = text.indexOf("ALLOW:");
const blockIndex = text.indexOf("BLOCK:");
const markerIndex =
allowIndex === -1
? blockIndex
: blockIndex === -1
? allowIndex
: Math.min(allowIndex, blockIndex);
const contractText = markerIndex >= 0 ? text.slice(markerIndex).trim() : text;
const contractFirstLine = contractText.split(/\r?\n/, 1)[0].trim();
if (firstLine.startsWith("ALLOW:")) {
return { ok: true, reason: null, rawOutput: text, firstLine };
}
if (firstLine.startsWith("BLOCK:")) {
const reason = firstLine.slice("BLOCK:".length).trim() || text;
return {
ok: false,
rawOutput: text,
firstLine,
reason: `Claude Code turn-end review found issues that still need fixes before ending this Codex turn: ${reason}`
};
}
if (contractFirstLine.startsWith("ALLOW:")) {
return { ok: true, reason: null, rawOutput: text, firstLine: contractFirstLine };
}
if (contractFirstLine.startsWith("BLOCK:")) {
const reason =
contractFirstLine.slice("BLOCK:".length).trim() || contractText;
return {
ok: false,
rawOutput: text,
firstLine: contractFirstLine,
reason: `Claude Code turn-end review found issues that still need fixes before ending this Codex turn: ${reason}`
};
}
return {
ok: false,
rawOutput: text,
firstLine,
reason:
"The turn-end Claude Code review returned an unexpected answer. Run $cc:review --wait manually or bypass the gate."
};
}
// ---------------------------------------------------------------------------
// Review execution via Claude CLI
// --------------------------------------------------------------#!/usr/bin/env node
/**
* Copyright 2026 Sendbird, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import process from "node:process";
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,
listJobs,
patchJob,
setCurrentSession,
writeTurnBaseline,
} from "../scripts/lib/state.mjs";
import { getWorkingTreeFingerprint } from "../scripts/lib/git.mjs";
import { nowIso, SESSION_ID_ENV } from "../scripts/lib/tracked-jobs.mjs";
import { resolveWorkspaceRoot } from "../scripts/lib/workspace.mjs";
const MAX_LISTED_JOBS = 3;
const SKIP_INTERACTIVE_HOOKS_ENV = "CLAUDE_COMPANION_SKIP_INTERACTIVE_HOOKS";
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
function isExplicitClaudeStatusRequest(prompt) {
const text = String(prompt ?? "").toLowerCase();
return text.includes("$cc:status") || text.includes("$cc:result");
}
function summarizeJob(job) {
const parts = [job.id];
if (job.kindLabel) parts.push(job.kindLabel);
if (job.summary) parts.push(job.summary);
return parts.join(" | ");
}
function buildAdditionalContext(jobs) {
const listed = jobs.slice(0, MAX_LISTED_JOBS).map((job) => `- ${summarizeJob(job)}`);
const remaining = jobs.length - listed.length;
const intro =
jobs.length === 1
? "A Claude Code background job from this session has finished and has not been surfaced yet."
: `${jobs.length} Claude Code background jobs from this session have finished and have not been surfaced yet.`;
const guidance =
jobs.length === 1
? `Before handling the new request, briefly mention that ${jobs[0].id} finished and ask whether the user wants to inspect its result first or continue with the new request. If they want the result, direct them to \`$cc:result ${jobs[0].id}\`. If the user is clearly asking about this finished work already, answer that directly instead of asking again. Do not bring this completion up again automatically after this turn.`
: "Before handling the new request, briefly mention that these Claude Code jobs finished and ask whether the user wants to inspect them first or continue with the new request. If they want to inspect them, direct them to `$cc:status` first, then `$cc:result <job-id>` for a specific finished job. If the user is clearly asking about this finished work already, answer that directly instead of asking again. Do not bring these completions up again automatically after this turn.";
return [
intro,
"",
"Finished jobs:",
...listed,
...(remaining > 0 ? [`- and ${remaining} more finished Claude Code job(s)`] : []),
"",
guidance,
].join("\n");
}
function selectUnreadCompletedJobs(jobs, sessionId) {
if (!sessionId) {
return [];
}
return jobs
.filter((job) => job.sessionId === sessionId)
.filter((job) => job.status === "completed")
.filter((job) => !job.resultViewedAt)
.filter((job) => !job.notifiedAt)
.sort((left, right) =>
String(right.updatedAt ?? right.completedAt ?? "").localeCompare(
String(left.updatedAt ?? left.completedAt ?? "")
)
);
}
function markJobsNotified(workspaceRoot, jobs) {
const timestamp = nowIso();
for (const job of jobs) {
patchJob(workspaceRoot, job.id, {
notifiedAt: timestamp,
});
}
}
function listJobsSafely(workspaceRoot) {
try {
// listJobs() triggers the PID-reuse-safe stale job reaper.
return listJobs(workspaceRoot);
} catch {
// Best effort only: unread-result steering should not fail a user prompt
// because stale job reaping hit a filesystem or process-inspection race.
return [];
}
}
function captureTurnBaseline(workspaceRoot, sessionId, cwd) {
if (!sessionId) {
return;
}
try {
const fingerprint = getWorkingTreeFingerprint(cwd);
writeTurnBaseline(workspaceRoot, sessionId, {
cwd,
workspaceRoot,
capturedAt: nowIso(),
fingerprint,
});
} catch {
// Baseline capture is best-effort. If it fails, Stop skips the review for
// this turn rather than reviewing a turn it cannot delimit.
}
}
async function main() {
const input = readHookInput();
if (cleanupAfterOfficialUninstall(ROOT_DIR)) {
return;
}
const cwd = input.cwd || process.cwd();
const workspaceRoot = resolveWorkspaceRoot(cwd);
const sessionId = input.session_id || process.env[SESSION_ID_ENV] || null;
const prompt = String(input.prompt ?? "");
if (
process.env[SKIP_INTERACTIVE_HOOKS_ENV] === "1" ||
!sessionId
) {
return;
}
try {
setCurrentSession(workspaceRoot, sessionId, {
hostOrigin: detectExternalHostOrigin(),
});
} catch {
// Best effort only: an invalid session id should not fail a user prompt.
}
const config = getConfig(workspaceRoot);
if (config.stopReviewGate) {
captureTurnBaseline(workspaceRoot, sessionId, cwd);
}
const jobs = listJobsSafely(workspaceRoot);
if (isExplicitClaudeStatusRequest(prompt)) {
return;
}
const unreadJobs = selectUnreadCompletedJobs(jobs, sessionId);
if (unreadJobs.length === 0) {
return;
}
markJobsNotified(workspaceRoot, unreadJobs);
process.stdout.write(`${buildAdditionalContext(unreadJobs)}\n`);
}
main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
);
process.exit(1);
});
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
An open-source plugin that runs inside Codex and lets you use Claude Code and Claude models for review, rescue, and tracked background workflows.
Splits a complex task across several specialized Claude Code agents working in parallel, then keeps going until the result is checked. You describe what you want in plain language instead of memorizing commands.
Turns a rough feature idea into a written spec through guided questions, then has the agent build it and checks the result against that spec. Instead of hand-checking whether the work is finished, you get an automatic evaluation gate that runs in your coding agent.
A plugin that runs coding tasks through multiple AI backends (Codex, Claude, Gemini, OpenCode) with a structured development workflow, parallel execution, browser automation and multi-session task checkpointing.
Coordinates AI agents with mechanical protocol enforcement, a workflow gate, memory validation and a session hook. Includes a catalog of specialist agents and has no runtime dependencies.
Bernstein is a Claude Code plugin for orchestrating AI agents. It adds skills to list and inspect agents, review and approve tasks, create tasks, track costs and budgets, and show alerts for failed or stalled work.
A Claude Code plugin that enforces finishing multi-step tasks with evidence and verification at each stage. It works as a procedure for Opus or any Claude model, not as chance.
Adjusts how third-party models behave inside Claude Code so they run closer to a native Opus session, covering subagent lifecycle, session hooks and tool interception. It does not replace your gateway, provider mapping or account setup.
Runs a PDCA workflow with phase gating, spec checks, and delegation to sub-agents for planning, QA, and deployment. Fires at session start and on prompts, edits, task creation, and phase transitions.