Claude Skill

codex-cc

Delegates coding tasks from Codex to local Claude Code in print mode while preserving Claude's normal runtime customizations by default. Use when: (1) You want Codex to call Claude Code locally, (2) You need Claude-side skills, plugins, MCP servers, custom commands, CLAUDE.md rul

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download Dianel555-DSkills-skills_codex-cc-d2fda23.zip · 15 KB
Part of dianel555/dskills — 14 skills

Install

skills CLI npx skills add https://github.com/Dianel555/DSkills/tree/main/skills/codex-cc
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dianel555-dskills@llmmart
Git git clone https://github.com/Dianel555/DSkills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dianel555/dskills collection as a plugin from our marketplace. Git is the plain clone.

README

codex-cc

A Codex-facing DSkills bridge that shells out to local Claude Code and returns a stable JSON envelope.

Overview

codex-cc is for the inverse direction of cc-codex: Codex calls Claude Code locally.

By default the bridge preserves Claude Code's normal runtime behavior. It does not force --safe-mode, --bare, --disable-slash-commands, or --strict-mcp-config, so trusted-workspace customizations such as CLAUDE.md, skills, plugins, MCP servers, custom commands, and rules continue to load the same way Claude normally would.

Two runtime caveats still come from Claude Code itself:

  • claude -p skips the interactive trust dialog, so only use this in directories you already trust.
  • Invalid settings files may be silently ignored in print mode, so if inheritance seems missing, validate the workspace .claude configuration first.

Installation

Copy this skill to your Codex skills directory:

cp -r skills/codex-cc ~/.codex/skills/

The bridge expects a working local Claude Code installation available as claude on PATH.

Usage

Basic

python scripts/claude_bridge.py --cd "/path/to/project" --PROMPT "Analyze the auth flow"

Resume a Claude session

# Initial turn
python scripts/claude_bridge.py --cd "/project" --PROMPT "Review the failing tests"

# Follow-up turn using the returned SESSION_ID
python scripts/claude_bridge.py --cd "/project" --SESSION_ID "uuid-from-response" --PROMPT "Now write the minimal fix"

Permission overrides are explicit

python scripts/claude_bridge.py --cd "/project" --PROMPT "Run the test suite" --permission-mode plan
python scripts/claude_bridge.py --cd "/project" --PROMPT "Run unattended in sandbox" --dangerously-skip-permissions

Claude management passthrough

python scripts/claude_bridge.py mcp list
python scripts/claude_bridge.py plugin list
python scripts/claude_bridge.py plugin marketplace list

Parameters

Parameter Required Description
--PROMPT Yes* Task instruction for Claude Code
--cd Yes* Workspace root used for both process cwd and --add-dir
--SESSION_ID No Resume an existing Claude conversation
--model No Claude model override. Aliases (e.g. haiku, sonnet) are resolved by Claude Code against the active endpoint, so a custom ANTHROPIC_BASE_URL may map them to a differently-named backend model
--permission-mode No Claude permission mode override
--dangerously-skip-permissions No Opt-in permission bypass
--timeout No Bridge-level timeout in seconds; omit it to wait without a bridge deadline
--stream-file No Raw Claude stream-json JSONL destination; omit it to create a temporary file
--return-all-messages No Include all parsed stream records in the returned envelope

* Not required for mcp / plugin passthrough invocations.

Output Format

Successful task execution:

{
  "success": true,
  "SESSION_ID": "uuid",
  "agent_messages": "Claude response text",
  "stream_file": "/tmp/claude_stream_....jsonl",
  "stderr": "optional diagnostic text"
}

The bridge reads Claude's streaming output incrementally and treats the final result record as the authoritative completion message. Intermediate assistant records remain available in stream_file and, when --return-all-messages is set, in all_messages.

When the bridge deadline fires, the error also reports the last transport-level retry it observed (for example claude timed out after 300.0s (last transport error: HTTP 502 server_error, attempt 1/10)), so an upstream or proxy outage is distinguishable from a genuinely slow task.

Failed task execution:

{
  "success": false,
  "SESSION_ID": "uuid-if-claude-was-launched",
  "error": "Failure reason",
  "stream_file": "/tmp/claude_stream_....jsonl",
  "stderr": "optional diagnostic text"
}

Passthrough output:

{
  "success": true,
  "output": "...",
  "error": "",
  "returncode": 0
}

Skill manifest

Quick Start

python scripts/claude_bridge.py --cd "/path/to/project" --PROMPT "Analyze auth flow"

Output: JSON with success, SESSION_ID, agent_messages, stream_file (the raw Claude JSONL stream), and optional all_messages, stderr, or error.

Runtime Contract

  • Default execution uses claude -p and does not force --safe-mode, --bare, --disable-slash-commands, or --strict-mcp-config.
  • Claude Code therefore keeps its normal loading path for trusted-workspace customizations such as CLAUDE.md, skills, plugins, MCP servers, custom commands, and rules.
  • claude -p skips the interactive trust dialog and silently ignores invalid settings files, so use this only in workspaces you already trust and whose .claude settings already validate.
  • The bridge requests stream-json, persists each record immediately, and only treats Claude's final result record as a completed answer. Intermediate assistant records never mask a failed or incomplete turn.
  • On Windows the PROMPT is delivered through stdin when claude resolves to a .cmd/.bat shim, so the prompt never passes through cmd.exe quoting and cannot hit the command-line length limit.

Parameters

usage: claude_bridge.py [-h] --PROMPT PROMPT --cd CD [--SESSION_ID SESSION_ID]
                        [--model MODEL]
                        [--permission-mode {,acceptEdits,auto,bypassPermissions,manual,dontAsk,plan}]
                        [--dangerously-skip-permissions] [--timeout TIMEOUT]
                        [--stream-file STREAM_FILE] [--return-all-messages]
                        {mcp,plugin} ...

options:
  --PROMPT PROMPT                Instruction for the task to send to Claude Code.
  --cd CD                        Workspace root for Claude Code (cwd + --add-dir).
  --SESSION_ID SESSION_ID        Resume a conversation by session UUID.
  --model MODEL                  Claude model override. Aliases resolve against the active endpoint.
  --permission-mode ...          Claude permission mode override.
  --dangerously-skip-permissions Bypass Claude permission checks.
  --timeout TIMEOUT              Bridge-level timeout in seconds. Omit for no bridge deadline.
  --stream-file STREAM_FILE      Raw Claude stream-json JSONL destination.
  --return-all-messages          Include parsed stream records in the result.

subcommands:
  mcp                            Thin passthrough to `claude mcp`.
  plugin                         Thin passthrough to `claude plugin`.

Sessions

Capture SESSION_ID from the first successful response and reuse it for follow-ups. SESSION_ID is empty when a run never established a session (claude produced no stream output): resume only with an id the bridge actually returned.

# New Claude session
python scripts/claude_bridge.py --cd "/project" --PROMPT "Inspect failing tests"

# Resume the same Claude session
python scripts/claude_bridge.py --cd "/project" --SESSION_ID "uuid-from-response" --PROMPT "Now propose the fix"

Passthrough

python scripts/claude_bridge.py mcp list
python scripts/claude_bridge.py plugin list
Files (dskills)
  • scripts
    • claude_bridge.py 21 KB
      """Claude Bridge Script for Codex-facing DSkills.
      
      Wraps the local Claude Code CLI in a stable JSON envelope so Codex can
      delegate work while preserving Claude's normal runtime customizations.
      """
      
      from __future__ import annotations
      
      import argparse
      import contextlib
      import json
      import os
      import queue
      import shutil
      import subprocess
      import sys
      import tempfile
      import threading
      import time
      import uuid
      from collections.abc import Iterator, Sequence
      from pathlib import Path
      
      PERMISSION_MODES = [
          "acceptEdits",
          "auto",
          "bypassPermissions",
          "manual",
          "dontAsk",
          "plan",
      ]
      
      
      def _is_windows() -> bool:
          """Platform seam: patched in tests so no test mutates the shared os.name."""
          return os.name == "nt"
      
      
      def _windows_bin_dir_candidates(home: str, env: dict) -> list[str]:
          """Candidate launcher directories for Windows, native installer first.
      
          Returns plain strings so the ordering contract stays testable on any platform.
          """
          candidates = [os.path.join(home, ".local", "bin")]
          if prefix := env.get("NPM_CONFIG_PREFIX") or env.get("npm_config_prefix"):
              candidates.append(prefix)
          if appdata := env.get("APPDATA"):
              candidates.append(os.path.join(appdata, "npm"))
          if localappdata := env.get("LOCALAPPDATA"):
              candidates.append(os.path.join(localappdata, "npm"))
          if programfiles := env.get("ProgramFiles"):
              candidates.append(os.path.join(programfiles, "nodejs"))
          return candidates
      
      
      def _get_windows_bin_paths() -> list[Path]:
          """Resolve the Windows launcher candidate directories to existing paths."""
          if not _is_windows():
              return []
          return [Path(entry) for entry in _windows_bin_dir_candidates(str(Path.home()), os.environ)]
      
      
      def _augment_path_env(env: dict) -> None:
          """Prepend known Claude Code install directories to PATH if missing."""
          if not _is_windows():
              return
          path_key = next((key for key in env if key.upper() == "PATH"), "PATH")
          path_entries = [entry for entry in env.get(path_key, "").split(os.pathsep) if entry]
          lower_set = {entry.lower() for entry in path_entries}
          for candidate in _get_windows_bin_paths():
              if candidate.is_dir() and str(candidate).lower() not in lower_set:
                  path_entries.insert(0, str(candidate))
                  lower_set.add(str(candidate).lower())
          env[path_key] = os.pathsep.join(path_entries)
      
      
      def _resolve_executable(name: str, env: dict) -> str:
          """Resolve executable path, checking npm dirs for .cmd/.bat on Windows."""
          if os.path.isabs(name) or os.sep in name or (os.altsep and os.altsep in name):
              return name
          path_key = next((key for key in env if key.upper() == "PATH"), "PATH")
          path_val = env.get(path_key)
          win_exts = {".exe", ".cmd", ".bat", ".com"}
          if resolved := shutil.which(name, path=path_val):
              if _is_windows():
                  suffix = Path(resolved).suffix.lower()
                  if not suffix:
                      resolved_dir = str(Path(resolved).parent)
                      for ext in (".cmd", ".bat", ".exe", ".com"):
                          candidate = Path(resolved_dir) / f"{name}{ext}"
                          if candidate.is_file():
                              return str(candidate)
                  elif suffix not in win_exts:
                      return resolved
              return resolved
          if _is_windows():
              for base in _get_windows_bin_paths():
                  for ext in (".cmd", ".bat", ".exe", ".com"):
                      candidate = base / f"{name}{ext}"
                      if candidate.is_file():
                          return str(candidate)
          return name
      
      
      def windows_escape(value: str) -> str:
          """Escape control characters that cmd.exe would otherwise mangle."""
          value = value.replace("\n", "\\n")
          value = value.replace("\r", "\\r")
          value = value.replace("\t", "\\t")
          return value
      
      
      def _prepare_popen_cmd(cmd: Sequence[str], env: dict):
          """Resolve executable and wrap Windows .cmd/.bat via cmd.exe."""
          popen_cmd = list(cmd)
          exe_path = _resolve_executable(popen_cmd[0], env)
          popen_cmd[0] = exe_path
      
          if _is_windows() and Path(exe_path).suffix.lower() in {".cmd", ".bat"}:
              popen_cmd = [windows_escape(arg) for arg in popen_cmd]
      
              def _cmd_quote(arg: str) -> str:
                  # Port of Rust std append_bat_arg (CVE-2024-24576 fix), adapted to the
                  # npm .cmd shim re-parsing (%* then node/MSVC CRT rules). Always quote;
                  # "" for an embedded quote, double backslash runs around quotes, and
                  # %%cd:~,% for % so no %VAR% can form. ^ needs no escaping inside quotes.
                  if not arg:
                      return '""'
                  out = ['"']
                  backslashes = 0
                  for ch in arg:
                      if ch == "\\":
                          backslashes += 1
                          continue
                      if ch == '"':
                          out.append("\\" * (backslashes * 2))
                          out.append('""')
                      elif ch == "%":
                          out.append("\\" * backslashes)
                          out.append("%%cd:~,%")
                      else:
                          out.append("\\" * backslashes)
                          out.append(ch)
                      backslashes = 0
                  out.append("\\" * (backslashes * 2))
                  out.append('"')
                  return "".join(out)
      
              cmdline = " ".join(_cmd_quote(arg) for arg in popen_cmd)
              comspec = env.get("COMSPEC", "cmd.exe")
              return f'"{comspec}" /d /s /c "{cmdline}"'
          return popen_cmd
      
      
      def configure_windows_stdio() -> None:
          """Configure stdout/stderr to use UTF-8 encoding on Windows."""
          if not _is_windows():
              return
          for stream in (sys.stdout, sys.stderr):
              reconfigure = getattr(stream, "reconfigure", None)
              if callable(reconfigure):
                  with contextlib.suppress(ValueError, OSError):
                      reconfigure(encoding="utf-8")
      
      
      def emit(result: dict) -> None:
          print(json.dumps(result, indent=2, ensure_ascii=False))
      
      
      def _normalize_workspace(path_value) -> Path:
          return Path(path_value).expanduser().resolve()
      
      
      def build_claude_cmd(args) -> tuple[list[str], str]:
          """Build `claude -p ...` argv from parsed run args."""
          workspace = str(_normalize_workspace(args.cd))
          session_id = args.SESSION_ID or str(uuid.uuid4())
      
          cmd = [
              "claude",
              "-p",
              "--output-format",
              "stream-json",
              "--verbose",
              "--add-dir",
              workspace,
          ]
      
          if args.model:
              cmd.extend(["--model", args.model])
          if args.permission_mode:
              cmd.extend(["--permission-mode", args.permission_mode])
          if args.dangerously_skip_permissions:
              cmd.append("--dangerously-skip-permissions")
      
          if args.SESSION_ID:
              cmd.extend(["--resume", args.SESSION_ID])
          else:
              cmd.extend(["--session-id", session_id])
      
          cmd.append(args.PROMPT)
          return cmd, session_id
      
      
      def _resumable_session_id(args, session_id: str, events_seen: bool) -> str:
          """Advertise SESSION_ID only when the conversation actually exists.
      
          A caller-supplied --SESSION_ID always names a real conversation. A
          pre-generated uuid (--session-id) names one only once claude has emitted at
          least one stream record; with none, claude never started, so returning the
          uuid would advertise a session that can never resume.
          """
          return session_id if (args.SESSION_ID or events_seen) else ""
      
      
      def _coerce_stream_text(value) -> str:
          if value is None:
              return ""
          if isinstance(value, bytes):
              return value.decode("utf-8", "replace")
          return str(value)
      
      
      def _stop_process(process: subprocess.Popen) -> None:
          """Stop a bridge-owned process without waiting indefinitely.
      
          On the Windows .cmd/.bat path the direct child is a cmd.exe wrapper;
          terminate() would kill only the shell and orphan the node launcher and
          claude.exe while they hold the stdout pipe open. Kill the whole tree.
          """
          if process.poll() is not None:
              return
          if _is_windows():
              subprocess.run(
                  ["taskkill", "/T", "/F", "/PID", str(process.pid)],
                  stdin=subprocess.DEVNULL,
                  stdout=subprocess.DEVNULL,
                  stderr=subprocess.DEVNULL,
                  check=False,
              )
          else:
              process.terminate()
          try:
              process.wait(timeout=5)
          except subprocess.TimeoutExpired:
              process.kill()
              with contextlib.suppress(subprocess.TimeoutExpired):
                  process.wait(timeout=5)
      
      
      def _stream_claude_output(
          popen_cmd,
          workspace: str,
          env: dict,
          timeout: float | None,
          stderr_sink: list[str],
          stdin_prompt: str | None = None,
      ) -> Iterator[str]:
          """Yield Claude JSONL records while draining stderr concurrently."""
          process = subprocess.Popen(
              popen_cmd,
              shell=False,
              cwd=workspace,
              stdin=subprocess.PIPE if stdin_prompt is not None else subprocess.DEVNULL,
              stdout=subprocess.PIPE,
              stderr=subprocess.PIPE,
              text=True,
              encoding="utf-8",
              errors="replace",
              env=env,
          )
          if stdin_prompt is not None and process.stdin is not None:
      
              def write_prompt() -> None:
                  with contextlib.suppress(BrokenPipeError, OSError, ValueError):
                      process.stdin.write(stdin_prompt)
                  with contextlib.suppress(BrokenPipeError, OSError, ValueError):
                      process.stdin.close()
      
              threading.Thread(target=write_prompt, daemon=True).start()
      
          stdout_queue: queue.Queue[str | None] = queue.Queue()
      
          def read_stdout() -> None:
              assert process.stdout is not None
              try:
                  for line in process.stdout:
                      stdout_queue.put(line.rstrip("\r\n"))
              finally:
                  stdout_queue.put(None)
      
          def read_stderr() -> None:
              assert process.stderr is not None
              for line in process.stderr:
                  text = line.rstrip("\r\n")
                  if text:
                      stderr_sink.append(text)
      
          stdout_thread = threading.Thread(target=read_stdout, daemon=True)
          stderr_thread = threading.Thread(target=read_stderr, daemon=True)
          stdout_thread.start()
          stderr_thread.start()
          deadline = time.monotonic() + timeout if timeout is not None else None
      
          try:
              while True:
                  wait_for = None
                  if deadline is not None:
                      wait_for = deadline - time.monotonic()
                      if wait_for <= 0:
                          raise subprocess.TimeoutExpired(popen_cmd, timeout)
                  try:
                      line = stdout_queue.get(timeout=wait_for)
                  except queue.Empty as exc:
                      raise subprocess.TimeoutExpired(popen_cmd, timeout) from exc
                  if line is None:
                      break
                  yield line
      
              try:
                  process.wait(timeout=5)
              except subprocess.TimeoutExpired:
                  _stop_process(process)
              stderr_thread.join(timeout=1)
          except (KeyboardInterrupt, subprocess.TimeoutExpired):
              _stop_process(process)
              raise
          finally:
              if process.poll() is None:
                  _stop_process(process)
      
      
      def _create_stream_file(requested_path: str) -> Path:
          if requested_path:
              return Path(requested_path).expanduser().resolve()
          descriptor, path = tempfile.mkstemp(prefix="claude_stream_", suffix=".jsonl")
          os.close(descriptor)
          return Path(path)
      
      
      def _event_text(value) -> str:
          if value is None:
              return ""
          if isinstance(value, str):
              return value.strip()
          return json.dumps(value, ensure_ascii=False)
      
      
      def _describe_retry_events(retry_events: list[dict]) -> str:
          """Summarize the last transport-level retry so a timeout is diagnosable."""
          if not retry_events:
              return ""
          last = retry_events[-1]
          status = last.get("error_status")
          detail = _event_text(last.get("error"))
          attempt = last.get("attempt")
          max_retries = last.get("max_retries")
          parts = []
          if status is not None:
              parts.append(f"HTTP {status}")
          if detail:
              parts.append(detail)
          summary = " ".join(parts) or "unknown transport error"
          if attempt is not None and max_retries is not None:
              summary += f", attempt {attempt}/{max_retries}"
          return f"last transport error: {summary}"
      
      
      def run_passthrough(subcommand: str, extra: list[str], timeout: float = 120.0) -> None:
          """Thin passthrough to `claude <subcommand> ...` for management flows."""
          env = os.environ.copy()
          _augment_path_env(env)
          cmd = ["claude", subcommand] + extra
          popen_cmd = _prepare_popen_cmd(cmd, env)
          try:
              cp = subprocess.run(
                  popen_cmd,
                  shell=False,
                  stdin=subprocess.DEVNULL,
                  capture_output=True,
                  check=False,
                  timeout=timeout,
                  text=True,
                  encoding="utf-8",
                  errors="replace",
                  env=env,
              )
              emit(
                  {
                      "success": cp.returncode == 0,
                      "output": cp.stdout or "",
                      "error": cp.stderr or "",
                      "returncode": cp.returncode,
                  }
              )
          except subprocess.TimeoutExpired:
              emit(
                  {
                      "success": False,
                      "output": "",
                      "error": f"claude {subcommand} timed out",
                      "returncode": -1,
                  }
              )
          except FileNotFoundError:
              emit(
                  {
                      "success": False,
                      "output": "",
                      "error": "claude binary not found in PATH",
                      "returncode": 127,
                  }
              )
      
      
      def cmd_run(args) -> None:
          workspace = Path(args.cd).expanduser()
          if not workspace.exists():
              emit(
                  {
                      "success": False,
                      "error": f"The workspace root directory `{workspace.resolve(strict=False)}` does not exist. Please check the path and try again.",
                  }
              )
              return
          if not workspace.is_dir():
              emit(
                  {
                      "success": False,
                      "error": f"The workspace root `{workspace.resolve()}` is not a directory.",
                  }
              )
              return
      
          workspace = workspace.resolve()
          cmd, session_id = build_claude_cmd(args)
          env = os.environ.copy()
          _augment_path_env(env)
          # On the Windows .cmd/.bat shim path the whole prompt would ride inside the
          # cmd.exe command line, which breaks at ~8k chars and re-parses quoting.
          # claude -p has no `-` positional convention: piping text with no positional
          # prompt argument makes stdin the prompt. Deliver it there instead, which
          # also preserves real newlines. Passthrough (mcp/plugin) is never `-p`.
          stdin_prompt = None
          if _is_windows() and len(cmd) > 2 and cmd[0] == "claude" and cmd[1] == "-p":
              resolved = _resolve_executable(cmd[0], env)
              if Path(resolved).suffix.lower() in {".cmd", ".bat"}:
                  stdin_prompt = cmd.pop()
          popen_cmd = _prepare_popen_cmd(cmd, env)
          try:
              stream_file = _create_stream_file(getattr(args, "stream_file", ""))
              stream = stream_file.open("w", encoding="utf-8", newline="\n")
          except OSError as exc:
              emit({"success": False, "error": f"Could not open Claude stream file: {exc}"})
              return
          stderr_lines: list[str] = []
          all_messages: list[dict] = []
          parse_errors: list[str] = []
          retry_events: list[dict] = []
          result_seen = False
          result_success = False
          result_text = ""
      
          try:
              with stream:
                  for line in _stream_claude_output(
                      popen_cmd,
                      str(workspace),
                      env,
                      args.timeout,
                      stderr_lines,
                      stdin_prompt,
                  ):
                      stream.write(f"{line}\n")
                      stream.flush()
                      if not line.strip():
                          continue
                      try:
                          event = json.loads(line)
                      except json.JSONDecodeError as exc:
                          parse_errors.append(f"line {exc.lineno}: {exc.msg}")
                          continue
                      if not isinstance(event, dict):
                          parse_errors.append("stream record was not a JSON object")
                          continue
                      all_messages.append(event)
                      event_session_id = event.get("session_id")
                      if isinstance(event_session_id, str) and event_session_id:
                          session_id = event_session_id
                      if event.get("subtype") == "api_retry":
                          retry_events.append(event)
                      if event.get("type") == "result":
                          result_seen = True
                          result_success = event.get("subtype") == "success" and not event.get("is_error", False)
                          result_text = _event_text(event.get("result"))
          except subprocess.TimeoutExpired as exc:
              error = f"claude timed out after {args.timeout}s"
              if retry_detail := _describe_retry_events(retry_events):
                  error += f" ({retry_detail})"
              result = {
                  "success": False,
                  "SESSION_ID": _resumable_session_id(args, session_id, bool(all_messages)),
                  "error": error,
                  "stream_file": str(stream_file),
              }
              stderr = "\n".join(stderr_lines).strip()
              if not stderr:
                  stderr = _coerce_stream_text(getattr(exc, "stderr", None)).strip()
              if getattr(args, "return_all_messages", False):
                  result["all_messages"] = all_messages
              if stderr:
                  result["stderr"] = stderr
              emit(result)
              return
          except KeyboardInterrupt:
              result = {
                  "success": False,
                  "SESSION_ID": _resumable_session_id(args, session_id, bool(all_messages)),
                  "error": "claude interrupted",
                  "stream_file": str(stream_file),
              }
              if getattr(args, "return_all_messages", False):
                  result["all_messages"] = all_messages
              emit(result)
              return
          except FileNotFoundError:
              emit(
                  {
                      "success": False,
                      "error": "claude binary not found in PATH",
                      "stream_file": str(stream_file),
                  }
              )
              return
      
          stderr = "\n".join(stderr_lines).strip()
          if result_seen and result_success and result_text:
              result = {
                  "success": True,
                  "SESSION_ID": _resumable_session_id(args, session_id, bool(all_messages)),
                  "agent_messages": result_text,
                  "stream_file": str(stream_file),
              }
              if getattr(args, "return_all_messages", False):
                  result["all_messages"] = all_messages
              if stderr:
                  result["stderr"] = stderr
              emit(result)
              return
      
          if result_seen:
              error = result_text or "Claude result contained no assistant text."
          else:
              error = "Claude stream ended without a result event."
              if parse_errors:
                  error += f" Parse errors: {'; '.join(parse_errors)}"
          result = {
              "success": False,
              "SESSION_ID": _resumable_session_id(args, session_id, bool(all_messages)),
              "error": error,
              "stream_file": str(stream_file),
          }
          if getattr(args, "return_all_messages", False):
              result["all_messages"] = all_messages
          if stderr:
              result["stderr"] = stderr
          emit(result)
      
      
      def main() -> None:
          configure_windows_stdio()
      
          if len(sys.argv) > 1 and sys.argv[1] in ("mcp", "plugin"):
              run_passthrough(sys.argv[1], sys.argv[2:])
              return
      
          parser = argparse.ArgumentParser(description="Claude Bridge")
          parser.add_argument(
              "--PROMPT",
              required=True,
              help="Instruction for the task to send to Claude Code.",
          )
          parser.add_argument(
              "--cd",
              required=True,
              type=Path,
              help="Workspace root for Claude Code (cwd + --add-dir).",
          )
          parser.add_argument("--SESSION_ID", default="", help="Resume a conversation by session UUID.")
          parser.add_argument(
              "--model",
              default="",
              help="Claude model override. Omit to inherit the configured default.",
          )
          parser.add_argument(
              "--permission-mode",
              default="",
              choices=[""] + PERMISSION_MODES,
              help="Claude permission mode override. Omit to preserve the configured default.",
          )
          parser.add_argument(
              "--dangerously-skip-permissions",
              action="store_true",
              help="Bypass Claude permission checks. Use only when the caller explicitly requests it.",
          )
          parser.add_argument(
              "--timeout",
              type=float,
              default=None,
              help="Bridge-level timeout in seconds. Omit to wait without a bridge deadline.",
          )
          parser.add_argument(
              "--stream-file",
              default="",
              help="Path for raw Claude stream-json records. Omit to create a temporary JSONL file.",
          )
          parser.add_argument(
              "--return-all-messages",
              action="store_true",
              help="Include parsed stream-json records in the returned JSON envelope.",
          )
      
          sub = parser.add_subparsers(dest="subcommand")
          sub.add_parser("mcp", help="Thin passthrough to `claude mcp`.")
          sub.add_parser("plugin", help="Thin passthrough to `claude plugin`.")
      
          args = parser.parse_args()
          cmd_run(args)
      
      
      if __name__ == "__main__":
          main()
      
  • tests
    • test_claude_bridge.py 24.7 KB
      """Regression tests for claude_bridge.py.
      
      Run: python -m pytest skills/codex-cc/tests/test_claude_bridge.py
      These tests mock subprocess execution; no real Claude process is launched.
      """
      
      import importlib.util
      import json
      import subprocess
      import sys
      import time
      import uuid
      from pathlib import Path
      from types import SimpleNamespace
      
      import pytest
      
      _SRC = Path(__file__).resolve().parents[1] / "scripts" / "claude_bridge.py"
      _spec = importlib.util.spec_from_file_location("claude_bridge", _SRC)
      cb = importlib.util.module_from_spec(_spec)
      _spec.loader.exec_module(cb)
      
      
      def _run_main(monkeypatch, capsys, argv):
          monkeypatch.setattr(sys, "argv", ["claude_bridge.py", *argv])
          cb.main()
          return json.loads(capsys.readouterr().out)
      
      
      def _stream_event(event):
          return json.dumps(event, ensure_ascii=False)
      
      
      def test_build_claude_cmd_new_session_generates_uuid(monkeypatch, tmp_path):
          fixed_uuid = uuid.UUID("11111111-1111-1111-1111-111111111111")
          monkeypatch.setattr(cb.uuid, "uuid4", lambda: fixed_uuid)
          args = SimpleNamespace(
              PROMPT="Analyze auth",
              cd=tmp_path,
              SESSION_ID="",
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=600.0,
          )
      
          cmd, session_id = cb.build_claude_cmd(args)
      
          workspace = str(tmp_path.resolve())
          assert cmd[:7] == [
              "claude",
              "-p",
              "--output-format",
              "stream-json",
              "--verbose",
              "--add-dir",
              workspace,
          ]
          assert cmd[cmd.index("--session-id") + 1] == str(fixed_uuid)
          assert cmd[-1] == "Analyze auth"
          assert session_id == str(fixed_uuid)
          assert "--resume" not in cmd
          assert "--safe-mode" not in cmd
          assert "--bare" not in cmd
          assert "--disable-slash-commands" not in cmd
          assert "--strict-mcp-config" not in cmd
          assert "--permission-mode" not in cmd
          assert "--dangerously-skip-permissions" not in cmd
      
      
      def test_build_claude_cmd_resume_preserves_session_and_opt_in_flags(tmp_path):
          session_id = "22222222-2222-2222-2222-222222222222"
          args = SimpleNamespace(
              PROMPT="Continue",
              cd=tmp_path,
              SESSION_ID=session_id,
              model="sonnet",
              permission_mode="plan",
              dangerously_skip_permissions=True,
              timeout=42.0,
          )
      
          cmd, returned_session = cb.build_claude_cmd(args)
      
          assert cmd[cmd.index("--resume") + 1] == session_id
          assert "--session-id" not in cmd
          assert cmd[cmd.index("--model") + 1] == "sonnet"
          assert cmd[cmd.index("--permission-mode") + 1] == "plan"
          assert "--dangerously-skip-permissions" in cmd
          assert returned_session == session_id
      
      
      def test_missing_workspace_fails_without_launch(monkeypatch, capsys, tmp_path):
          launched = {"value": False}
      
          def fake_stream(*args, **kwargs):
              launched["value"] = True
              raise AssertionError("Claude should not be called")
      
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT="Analyze auth",
              cd=tmp_path / "missing",
              SESSION_ID="",
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=600.0,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
          assert out["success"] is False
          assert "does not exist" in out["error"]
          assert "agent_messages" not in out
          assert launched["value"] is False
      
      
      def test_success_envelope_and_workspace_coherence(monkeypatch, capsys, tmp_path):
          fixed_uuid = uuid.UUID("33333333-3333-3333-3333-333333333333")
          captured = {}
          stream_file = tmp_path / "claude-stream.jsonl"
      
          def fake_stream(popen_cmd, workspace, env, timeout, stderr_sink, stdin_prompt=None):
              captured["popen_cmd"] = popen_cmd
              captured["workspace"] = workspace
              captured["timeout"] = timeout
              stderr_sink.append("warning")
              yield _stream_event({"type": "system", "subtype": "init", "session_id": str(fixed_uuid)})
              yield _stream_event(
                  {
                      "type": "assistant",
                      "session_id": str(fixed_uuid),
                      "message": {"content": [{"type": "text", "text": "intermediate"}]},
                  }
              )
              yield _stream_event(
                  {
                      "type": "result",
                      "subtype": "success",
                      "is_error": False,
                      "session_id": str(fixed_uuid),
                      "result": "final answer",
                  }
              )
      
          monkeypatch.setattr(cb.uuid, "uuid4", lambda: fixed_uuid)
          monkeypatch.setattr(cb, "_prepare_popen_cmd", lambda cmd, env: cmd)
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT="Analyze auth",
              cd=tmp_path,
              SESSION_ID="",
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=600.0,
              stream_file=str(stream_file),
              return_all_messages=True,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
          workspace = str(tmp_path.resolve())
      
          assert out["success"] is True
          assert out["SESSION_ID"] == str(fixed_uuid)
          assert out["agent_messages"] == "final answer"
          assert out["stream_file"] == str(stream_file)
          assert [item["type"] for item in out["all_messages"]] == [
              "system",
              "assistant",
              "result",
          ]
          assert out["stderr"] == "warning"
          assert captured["workspace"] == workspace
          assert captured["popen_cmd"][captured["popen_cmd"].index("--add-dir") + 1] == workspace
          assert stream_file.read_text(encoding="utf-8").count("\n") == 3
      
      
      def test_result_error_is_not_masked_by_assistant_message(monkeypatch, capsys, tmp_path):
          session_id = "33333333-3333-3333-3333-333333333333"
      
          def fake_stream(*args, **kwargs):
              yield _stream_event(
                  {
                      "type": "assistant",
                      "session_id": session_id,
                      "message": {"content": [{"type": "text", "text": "Working on it"}]},
                  }
              )
              yield _stream_event(
                  {
                      "type": "result",
                      "subtype": "error_during_execution",
                      "is_error": True,
                      "session_id": session_id,
                      "result": "upstream failed",
                  }
              )
      
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT="Analyze auth",
              cd=tmp_path,
              SESSION_ID=session_id,
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=None,
              stream_file="",
              return_all_messages=False,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
      
          assert out["success"] is False
          assert out["SESSION_ID"] == session_id
          assert out["error"] == "upstream failed"
          assert "agent_messages" not in out
          assert Path(out["stream_file"]).is_file()
      
      
      def test_timeout_failure_is_not_reported_as_success(monkeypatch, capsys, tmp_path):
          fixed_uuid = uuid.UUID("44444444-4444-4444-4444-444444444444")
      
          def fake_stream(popen_cmd, workspace, env, timeout, stderr_sink, stdin_prompt=None):
              yield _stream_event({"type": "system", "subtype": "init", "session_id": str(fixed_uuid)})
              stderr_sink.append("still running")
              raise subprocess.TimeoutExpired(cmd=popen_cmd, timeout=5, stderr="still running")
      
          monkeypatch.setattr(cb.uuid, "uuid4", lambda: fixed_uuid)
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT="Analyze auth",
              cd=tmp_path,
              SESSION_ID="",
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=5.0,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
          assert out["success"] is False
          assert out["SESSION_ID"] == str(fixed_uuid)
          assert "timed out" in out["error"]
          assert "agent_messages" not in out
          assert Path(out["stream_file"]).read_text(encoding="utf-8").count("\n") == 1
      
      
      def test_timeout_reports_last_transport_error(monkeypatch, capsys, tmp_path):
          """A 502 during retry must be visible in the timeout envelope, not just the stream."""
      
          def fake_stream(popen_cmd, workspace, env, timeout, stderr_sink, stdin_prompt=None):
              yield _stream_event(
                  {
                      "type": "system",
                      "subtype": "api_retry",
                      "attempt": 1,
                      "max_retries": 10,
                      "retry_delay_ms": 576,
                      "error_status": 502,
                      "error": "server_error",
                  }
              )
              raise subprocess.TimeoutExpired(cmd=popen_cmd, timeout=5)
      
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT="Analyze auth",
              cd=tmp_path,
              SESSION_ID="",
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=5.0,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
      
          assert out["success"] is False
          assert "timed out" in out["error"]
          assert "HTTP 502" in out["error"]
          assert "server_error" in out["error"]
          assert "attempt 1/10" in out["error"]
      
      
      def test_timeout_without_retry_events_keeps_plain_error(monkeypatch, capsys, tmp_path):
          """Absent transport failures the timeout message stays unembellished."""
      
          def fake_stream(popen_cmd, workspace, env, timeout, stderr_sink, stdin_prompt=None):
              raise subprocess.TimeoutExpired(cmd=popen_cmd, timeout=5)
      
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT="Analyze auth",
              cd=tmp_path,
              SESSION_ID="",
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=5.0,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
      
          assert out["error"] == "claude timed out after 5.0s"
      
      
      def test_omitted_timeout_disables_bridge_deadline(monkeypatch, capsys, tmp_path):
          captured = {}
      
          def fake_stream(popen_cmd, workspace, env, timeout, stderr_sink, stdin_prompt=None):
              captured["timeout"] = timeout
              session_id = popen_cmd[popen_cmd.index("--session-id") + 1]
              yield _stream_event(
                  {
                      "type": "result",
                      "subtype": "success",
                      "is_error": False,
                      "session_id": session_id,
                      "result": "done",
                  }
              )
      
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
      
          out = _run_main(
              monkeypatch,
              capsys,
              ["--PROMPT", "Analyze auth", "--cd", str(tmp_path)],
          )
      
          assert captured["timeout"] is None
          assert out["success"] is True
      
      
      def test_interrupt_preserves_resume_session_id(monkeypatch, capsys, tmp_path):
          session_id = "55555555-5555-5555-5555-555555555555"
      
          def fake_stream(*args, **kwargs):
              raise KeyboardInterrupt
      
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT="Continue",
              cd=tmp_path,
              SESSION_ID=session_id,
              model="",
              permission_mode="plan",
              dangerously_skip_permissions=False,
              timeout=None,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
      
          assert out["success"] is False
          assert out["SESSION_ID"] == session_id
          assert out["error"] == "claude interrupted"
          assert Path(out["stream_file"]).is_file()
      
      
      def test_empty_stdout_is_a_failure(monkeypatch, capsys, tmp_path):
          def fake_stream(*args, **kwargs):
              if False:
                  yield ""
      
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT="Analyze auth",
              cd=tmp_path,
              SESSION_ID="",
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=5.0,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
          assert out["success"] is False
          # D5: claude never started, so the pre-generated uuid is a phantom session
          # and must not be advertised as resumable.
          assert out["SESSION_ID"] == ""
          assert "without a result event" in out["error"].lower()
          assert "agent_messages" not in out
          assert Path(out["stream_file"]).is_file()
      
      
      @pytest.mark.parametrize(
          ("subcommand", "extra"),
          [
              ("mcp", ["list", "--json"]),
              ("plugin", ["marketplace", "list"]),
          ],
      )
      def test_passthrough_argument_integrity(monkeypatch, capsys, subcommand, extra):
          captured = {}
      
          def fake_run(popen_cmd, **kwargs):
              captured["cmd"] = popen_cmd
              return SimpleNamespace(returncode=0, stdout="ok\n", stderr="")
      
          monkeypatch.setattr(cb, "_prepare_popen_cmd", lambda cmd, env: cmd)
          monkeypatch.setattr(cb.subprocess, "run", fake_run)
      
          out = _run_main(monkeypatch, capsys, [subcommand, *extra])
          assert captured["cmd"] == ["claude", subcommand, *extra]
          assert out["success"] is True
          assert out["returncode"] == 0
          assert out["output"] == "ok\n"
      
      
      def test_passthrough_timeout_returns_failure(monkeypatch, capsys):
          def fake_run(popen_cmd, **kwargs):
              raise subprocess.TimeoutExpired(cmd=popen_cmd, timeout=1)
      
          monkeypatch.setattr(cb, "_prepare_popen_cmd", lambda cmd, env: cmd)
          monkeypatch.setattr(cb.subprocess, "run", fake_run)
      
          out = _run_main(monkeypatch, capsys, ["plugin", "list"])
          assert out["success"] is False
          assert "timed out" in out["error"]
      
      
      def test_windows_resolution_falls_back_to_bin_dirs(monkeypatch, tmp_path):
          npm_dir = tmp_path / "npm"
          npm_dir.mkdir()
          claude_cmd = npm_dir / "claude.cmd"
          claude_cmd.write_text("@echo off\n", encoding="utf-8")
      
          monkeypatch.setattr(cb, "_is_windows", lambda: True)
          monkeypatch.setattr(cb, "_get_windows_bin_paths", lambda: [npm_dir])
          monkeypatch.setattr(cb.shutil, "which", lambda name, path=None: None)
      
          resolved = cb._resolve_executable("claude", {"PATH": ""})
          assert resolved == str(claude_cmd)
      
      
      def test_windows_bin_paths_prioritize_native_installer():
          """The native installer dir must be probed before npm dirs.
      
          Guards the regression where the candidate list held only npm locations, so a
          machine whose launcher lives in ~/.local/bin fell through to a bare name.
          """
          env = {
              "NPM_CONFIG_PREFIX": "C:\\npm-prefix",
              "APPDATA": "C:\\Users\\test\\AppData\\Roaming",
              "LOCALAPPDATA": "C:\\Users\\test\\AppData\\Local",
              "ProgramFiles": "C:\\Program Files",
          }
      
          candidates = cb._windows_bin_dir_candidates("C:\\Users\\test", env)
      
          native = candidates[0]
          assert native.endswith("bin") and ".local" in native
          npm_index = next(i for i, c in enumerate(candidates) if "npm-prefix" in c)
          assert npm_index > 0
      
      
      def test_windows_bin_dir_candidates_skip_unset_env():
          candidates = cb._windows_bin_dir_candidates(r"C:\Users\test", {})
      
          assert len(candidates) == 1
          assert candidates[0].endswith("bin")
          assert ".local" in candidates[0]
      
      
      def test_prepare_popen_cmd_escapes_windows_prompt(monkeypatch, tmp_path):
          claude_cmd = tmp_path / "claude.cmd"
          prompt = 'line 1\nline 2\t"quoted" 100%'
      
          monkeypatch.setattr(cb, "_is_windows", lambda: True)
          monkeypatch.setattr(cb, "_resolve_executable", lambda name, env: str(claude_cmd))
      
          popen_cmd = cb._prepare_popen_cmd(["claude", "-p", prompt], {"PATH": "", "COMSPEC": "cmd.exe"})
      
          assert isinstance(popen_cmd, str)
          assert "claude.cmd" in popen_cmd
          assert "line 1\\nline 2\\t" in popen_cmd
          assert "100%%" in popen_cmd
          assert "quoted" in popen_cmd
          assert "\n" not in popen_cmd
          assert "\t" not in popen_cmd
      
      
      def test_repository_catalog_registers_codex_cc():
          root = Path(__file__).resolve().parents[3]
          readme = (root / "README.md").read_text(encoding="utf-8")
          marketplace = json.loads((root / ".claude-plugin" / "marketplace.json").read_text(encoding="utf-8"))
      
          assert "[codex-cc](skills/codex-cc/)" in readme
          entry = next((item for item in marketplace["plugins"] if item["name"] == "codex-cc"), None)
          assert entry is not None
          assert entry["source"] == "./skills/codex-cc"
          assert "Claude Code" in entry["description"]
      
      
      # --- Windows .cmd shim: BatBadBut quoting, stdin prompt delivery, tree-kill ---
      
      
      class _FakeStdin:
          def __init__(self):
              self.chunks = []
              self.closed = False
      
          def write(self, value):
              self.chunks.append(value)
      
          def close(self):
              self.closed = True
      
      
      class _FakeProcess:
          def __init__(self, pid=4242):
              self.pid = pid
              self.stdin = _FakeStdin()
              self.stdout = iter([])
              self.stderr = iter([])
              self.kwargs = {}
      
          def poll(self):
              return 0
      
          def wait(self, timeout=None):
              return 0
      
          def terminate(self):
              pass
      
          def kill(self):
              pass
      
      
      def test_cmd_quote_rust_bat_encoding(monkeypatch):
          """Embedded quotes must survive the npm .cmd shim re-parse: a prompt holding
          "Out of scope" arrives as ONE argv entry, not three (the `of` argv leak)."""
          monkeypatch.setattr(cb, "_is_windows", lambda: True)
          monkeypatch.setattr(cb, "_resolve_executable", lambda name, env: r"C:\npm\claude.cmd")
          command = cb._prepare_popen_cmd(["claude", "-p", 'A "Out of scope" B 100% done'], {"COMSPEC": "cmd.exe"})
          assert command.startswith('"cmd.exe" /d /s /c "')
          assert '"A ""Out of scope"" B 100' in command
          assert '"^""' not in command
          assert "100%%cd:~,% done" in command
          assert "100%% done" not in command
      
      
      def test_cmd_quote_trailing_backslash(monkeypatch):
          monkeypatch.setattr(cb, "_is_windows", lambda: True)
          monkeypatch.setattr(cb, "_resolve_executable", lambda name, env: r"C:\npm\claude.cmd")
          command = cb._prepare_popen_cmd(["claude", "-p", "\\"], {})
          assert command.endswith('"-p" "\\\\""')
      
      
      def test_shim_run_moves_prompt_to_stdin(monkeypatch, capsys, tmp_path):
          """On the Windows shim path the PROMPT positional is dropped and delivered
          via stdin, so an 8k+ prompt cannot hit the cmd.exe limit and embedded quotes
          never reach cmd.exe."""
          captured = {}
      
          def fake_stream(popen_cmd, workspace, env, timeout, stderr_sink, stdin_prompt=None):
              captured["cmd"] = popen_cmd
              captured["stdin_prompt"] = stdin_prompt
              yield _stream_event(
                  {
                      "type": "result",
                      "subtype": "success",
                      "is_error": False,
                      "session_id": "77777777-7777-7777-7777-777777777777",
                      "result": "ok",
                  }
              )
      
          monkeypatch.setattr(cb, "_is_windows", lambda: True)
          monkeypatch.setattr(cb, "_resolve_executable", lambda name, env: r"C:\npm\claude.cmd")
          monkeypatch.setattr(cb, "_prepare_popen_cmd", lambda cmd, env: cmd)
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT='A "Out of scope" B',
              cd=tmp_path,
              SESSION_ID="",
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=600.0,
              stream_file="",
              return_all_messages=False,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
      
          assert out["success"] is True
          assert captured["stdin_prompt"] == 'A "Out of scope" B'
          assert captured["cmd"][1] == "-p"
          assert 'A "Out of scope" B' not in captured["cmd"]
      
      
      def test_shim_stdin_prompt_bytes_are_delivered(monkeypatch):
          proc = _FakeProcess()
      
          def fake_popen(command, **kwargs):
              proc.kwargs = kwargs
              return proc
      
          monkeypatch.setattr(cb.subprocess, "Popen", fake_popen)
      
          list(cb._stream_claude_output("cmdline", ".", {}, None, [], "x" * 9000))
      
          for _ in range(100):
              if proc.stdin.closed:
                  break
              time.sleep(0.01)
          assert "".join(proc.stdin.chunks) == "x" * 9000
          assert proc.stdin.closed
          assert proc.kwargs["stdin"] is subprocess.PIPE
      
      
      def test_posix_run_keeps_prompt_positional(monkeypatch, capsys, tmp_path):
          captured = {}
      
          def fake_stream(popen_cmd, workspace, env, timeout, stderr_sink, stdin_prompt=None):
              captured["cmd"] = popen_cmd
              captured["stdin_prompt"] = stdin_prompt
              yield _stream_event(
                  {
                      "type": "result",
                      "subtype": "success",
                      "is_error": False,
                      "session_id": "88888888-8888-8888-8888-888888888888",
                      "result": "ok",
                  }
              )
      
          monkeypatch.setattr(cb, "_is_windows", lambda: False)
          monkeypatch.setattr(cb, "_prepare_popen_cmd", lambda cmd, env: cmd)
          monkeypatch.setattr(cb, "_stream_claude_output", fake_stream)
          args = SimpleNamespace(
              PROMPT="Analyze auth",
              cd=tmp_path,
              SESSION_ID="",
              model="",
              permission_mode="",
              dangerously_skip_permissions=False,
              timeout=600.0,
              stream_file="",
              return_all_messages=False,
          )
      
          cb.cmd_run(args)
          out = json.loads(capsys.readouterr().out)
      
          assert out["success"] is True
          assert captured["stdin_prompt"] is None
          assert captured["cmd"][-1] == "Analyze auth"
      
      
      def test_windows_termination_kills_process_tree(monkeypatch):
          """terminate() on the cmd.exe wrapper orphans node/claude; taskkill /T /F is required."""
          monkeypatch.setattr(cb, "_is_windows", lambda: True)
          calls = []
      
          class _Proc(_FakeProcess):
              def __init__(self):
                  super().__init__(pid=4321)
                  self.exited = False
      
              def poll(self):
                  return 0 if self.exited else None
      
              def wait(self, timeout=None):
                  if timeout is not None and not self.exited:
                      raise subprocess.TimeoutExpired("cmd", timeout)
                  self.exited = True
                  return 0
      
          proc = _Proc()
      
          def fake_run(command, **kwargs):
              calls.append(command)
              proc.exited = True
      
          monkeypatch.setattr(cb.subprocess, "run", fake_run)
          cb._stop_process(proc)
      
          assert ["taskkill", "/T", "/F", "/PID", "4321"] in calls
      
      
      def test_posix_termination_uses_terminate(monkeypatch):
          monkeypatch.setattr(cb, "_is_windows", lambda: False)
          seen = {"terminate": False}
      
          class _Proc(_FakeProcess):
              def __init__(self):
                  super().__init__()
                  self.exited = False
      
              def poll(self):
                  return 0 if self.exited else None
      
              def terminate(self):
                  seen["terminate"] = True
                  self.exited = True
      
          cb._stop_process(_Proc())
      
          assert seen["terminate"] is True
      
      
      def test_passthrough_keeps_stdin_detached_on_windows(monkeypatch, capsys):
          """mcp/plugin passthrough must never take the stdin-prompt path: its argv has
          no PROMPT and cmd[1] is not -p, so stdin stays detached even on Windows."""
          captured = {}
      
          def fake_run(popen_cmd, **kwargs):
              captured["cmd"] = popen_cmd
              captured["kwargs"] = kwargs
              return SimpleNamespace(returncode=0, stdout="ok\n", stderr="")
      
          monkeypatch.setattr(cb, "_is_windows", lambda: True)
          monkeypatch.setattr(cb, "_resolve_executable", lambda name, env: r"C:\npm\claude.cmd")
          monkeypatch.setattr(cb.subprocess, "run", fake_run)
      
          out = _run_main(monkeypatch, capsys, ["mcp", "list"])
      
          assert out["success"] is True
          assert captured["kwargs"]["stdin"] is subprocess.DEVNULL
      
      
      def test_stop_process_falls_back_to_kill(monkeypatch):
          """When tree termination does not finish the process, kill() is the fallback."""
          monkeypatch.setattr(cb, "_is_windows", lambda: True)
          seen = {"kill": False}
      
          class _Proc(_FakeProcess):
              def __init__(self):
                  super().__init__(pid=5555)
                  self.waits = 0
      
              def poll(self):
                  return None
      
              def kill(self):
                  seen["kill"] = True
      
              def wait(self, timeout=None):
                  self.waits += 1
                  if self.waits == 1:
                      raise subprocess.TimeoutExpired("cmd", timeout)
                  return 0
      
          proc = _Proc()
          monkeypatch.setattr(cb.subprocess, "run", lambda command, **kwargs: None)
      
          cb._stop_process(proc)
      
          assert seen["kill"] is True
      
      
      def test_normal_completion_wait_timeout_triggers_tree_kill(monkeypatch):
          """A completion whose wrapper never exits must not hang the bridge: the wait
          timeout falls back to the same tree-kill."""
          monkeypatch.setattr(cb, "_is_windows", lambda: True)
          killed = []
      
          class _Proc(_FakeProcess):
              def __init__(self):
                  super().__init__(pid=6666)
      
              def poll(self):
                  return None
      
              def wait(self, timeout=None):
                  if timeout is not None:
                      raise subprocess.TimeoutExpired("cmd", timeout)
                  return 0
      
          proc = _Proc()
          monkeypatch.setattr(cb.subprocess, "Popen", lambda command, **kwargs: proc)
          monkeypatch.setattr(cb.subprocess, "run", lambda command, **kwargs: killed.append(command))
      
          list(cb._stream_claude_output("cmdline", ".", {}, None, []))
      
          assert ["taskkill", "/T", "/F", "/PID", "6666"] in killed
      
  • README.md 4 KB
    # codex-cc
    
    A Codex-facing DSkills bridge that shells out to local Claude Code and returns a stable JSON envelope.
    
    ## Overview
    
    `codex-cc` is for the inverse direction of `cc-codex`: Codex calls Claude Code locally.
    
    By default the bridge preserves Claude Code's normal runtime behavior. It does not force `--safe-mode`, `--bare`, `--disable-slash-commands`, or `--strict-mcp-config`, so trusted-workspace customizations such as `CLAUDE.md`, skills, plugins, MCP servers, custom commands, and rules continue to load the same way Claude normally would.
    
    Two runtime caveats still come from Claude Code itself:
    
    - `claude -p` skips the interactive trust dialog, so only use this in directories you already trust.
    - Invalid settings files may be silently ignored in print mode, so if inheritance seems missing, validate the workspace `.claude` configuration first.
    
    ## Installation
    
    Copy this skill to your Codex skills directory:
    
    ```bash
    cp -r skills/codex-cc ~/.codex/skills/
    ```
    
    The bridge expects a working local Claude Code installation available as `claude` on `PATH`.
    
    ## Usage
    
    ### Basic
    
    ```bash
    python scripts/claude_bridge.py --cd "/path/to/project" --PROMPT "Analyze the auth flow"
    ```
    
    ### Resume a Claude session
    
    ```bash
    # Initial turn
    python scripts/claude_bridge.py --cd "/project" --PROMPT "Review the failing tests"
    
    # Follow-up turn using the returned SESSION_ID
    python scripts/claude_bridge.py --cd "/project" --SESSION_ID "uuid-from-response" --PROMPT "Now write the minimal fix"
    ```
    
    ### Permission overrides are explicit
    
    ```bash
    python scripts/claude_bridge.py --cd "/project" --PROMPT "Run the test suite" --permission-mode plan
    python scripts/claude_bridge.py --cd "/project" --PROMPT "Run unattended in sandbox" --dangerously-skip-permissions
    ```
    
    ### Claude management passthrough
    
    ```bash
    python scripts/claude_bridge.py mcp list
    python scripts/claude_bridge.py plugin list
    python scripts/claude_bridge.py plugin marketplace list
    ```
    
    ## Parameters
    
    | Parameter | Required | Description |
    |-----------|----------|-------------|
    | `--PROMPT` | Yes* | Task instruction for Claude Code |
    | `--cd` | Yes* | Workspace root used for both process `cwd` and `--add-dir` |
    | `--SESSION_ID` | No | Resume an existing Claude conversation |
    | `--model` | No | Claude model override. Aliases (e.g. `haiku`, `sonnet`) are resolved by Claude Code against the active endpoint, so a custom `ANTHROPIC_BASE_URL` may map them to a differently-named backend model |
    | `--permission-mode` | No | Claude permission mode override |
    | `--dangerously-skip-permissions` | No | Opt-in permission bypass |
    | `--timeout` | No | Bridge-level timeout in seconds; omit it to wait without a bridge deadline |
    | `--stream-file` | No | Raw Claude `stream-json` JSONL destination; omit it to create a temporary file |
    | `--return-all-messages` | No | Include all parsed stream records in the returned envelope |
    
    `*` Not required for `mcp` / `plugin` passthrough invocations.
    
    ## Output Format
    
    Successful task execution:
    
    ```json
    {
      "success": true,
      "SESSION_ID": "uuid",
      "agent_messages": "Claude response text",
      "stream_file": "/tmp/claude_stream_....jsonl",
      "stderr": "optional diagnostic text"
    }
    ```
    
    The bridge reads Claude's streaming output incrementally and treats the final
    `result` record as the authoritative completion message. Intermediate
    `assistant` records remain available in `stream_file` and, when
    `--return-all-messages` is set, in `all_messages`.
    
    When the bridge deadline fires, the error also reports the last transport-level
    retry it observed (for example `claude timed out after 300.0s (last transport
    error: HTTP 502 server_error, attempt 1/10)`), so an upstream or proxy outage is
    distinguishable from a genuinely slow task.
    
    Failed task execution:
    
    ```json
    {
      "success": false,
      "SESSION_ID": "uuid-if-claude-was-launched",
      "error": "Failure reason",
      "stream_file": "/tmp/claude_stream_....jsonl",
      "stderr": "optional diagnostic text"
    }
    ```
    
    Passthrough output:
    
    ```json
    {
      "success": true,
      "output": "...",
      "error": "",
      "returncode": 0
    }
    ```
    
  • ruff.toml 656 B
    # Lint and format baseline for this skill.
    #
    # Kept explicit rather than relying on ruff's default rule set: CI installs ruff
    # unpinned, so a future release that widens its defaults would otherwise turn
    # this job red without any change to the code.
    line-length = 120
    target-version = "py311"
    
    [lint]
    select = [
        "E",   # pycodestyle errors
        "F",   # pyflakes
        "W",   # pycodestyle warnings
        "I",   # isort
        "N",   # pep8-naming
        "UP",  # pyupgrade
        "B",   # flake8-bugbear
        "A",   # flake8-builtins
        "C4",  # flake8-comprehensions
        "SIM", # flake8-simplify
    ]
    ignore = ["E501"] # line too long (handled by line-length above)
    
  • SKILL.md 3.6 KB
    ---
    name: codex-cc
    description: |
      Delegates coding tasks from Codex to local Claude Code in print mode while preserving Claude's normal runtime customizations by default. Use when: (1) You want Codex to call Claude Code locally, (2) You need Claude-side skills, plugins, MCP servers, custom commands, CLAUDE.md rules, or workspace settings to stay active, (3) You want resumable Claude sessions from Codex via SESSION_ID, (4) You need thin `claude mcp` / `claude plugin` passthrough from the same bridge.
    ---
    
    ## Quick Start
    
    ```bash
    python scripts/claude_bridge.py --cd "/path/to/project" --PROMPT "Analyze auth flow"
    ```
    
    **Output:** JSON with `success`, `SESSION_ID`, `agent_messages`, `stream_file`
    (the raw Claude JSONL stream), and optional `all_messages`, `stderr`, or `error`.
    
    ## Runtime Contract
    
    - Default execution uses `claude -p` and does **not** force `--safe-mode`, `--bare`, `--disable-slash-commands`, or `--strict-mcp-config`.
    - Claude Code therefore keeps its normal loading path for trusted-workspace customizations such as `CLAUDE.md`, skills, plugins, MCP servers, custom commands, and rules.
    - `claude -p` skips the interactive trust dialog and silently ignores invalid settings files, so use this only in workspaces you already trust and whose `.claude` settings already validate.
    - The bridge requests `stream-json`, persists each record immediately, and only treats Claude's final `result` record as a completed answer. Intermediate `assistant` records never mask a failed or incomplete turn.
    - On Windows the PROMPT is delivered through stdin when `claude` resolves to a `.cmd`/`.bat` shim, so the prompt never passes through cmd.exe quoting and cannot hit the command-line length limit.
    
    ## Parameters
    
    ```text
    usage: claude_bridge.py [-h] --PROMPT PROMPT --cd CD [--SESSION_ID SESSION_ID]
                            [--model MODEL]
                            [--permission-mode {,acceptEdits,auto,bypassPermissions,manual,dontAsk,plan}]
                            [--dangerously-skip-permissions] [--timeout TIMEOUT]
                            [--stream-file STREAM_FILE] [--return-all-messages]
                            {mcp,plugin} ...
    
    options:
      --PROMPT PROMPT                Instruction for the task to send to Claude Code.
      --cd CD                        Workspace root for Claude Code (cwd + --add-dir).
      --SESSION_ID SESSION_ID        Resume a conversation by session UUID.
      --model MODEL                  Claude model override. Aliases resolve against the active endpoint.
      --permission-mode ...          Claude permission mode override.
      --dangerously-skip-permissions Bypass Claude permission checks.
      --timeout TIMEOUT              Bridge-level timeout in seconds. Omit for no bridge deadline.
      --stream-file STREAM_FILE      Raw Claude stream-json JSONL destination.
      --return-all-messages          Include parsed stream records in the result.
    
    subcommands:
      mcp                            Thin passthrough to `claude mcp`.
      plugin                         Thin passthrough to `claude plugin`.
    ```
    
    ## Sessions
    
    Capture `SESSION_ID` from the first successful response and reuse it for follow-ups.
    `SESSION_ID` is empty when a run never established a session (claude produced no stream output):
    resume only with an id the bridge actually returned.
    
    ```bash
    # New Claude session
    python scripts/claude_bridge.py --cd "/project" --PROMPT "Inspect failing tests"
    
    # Resume the same Claude session
    python scripts/claude_bridge.py --cd "/project" --SESSION_ID "uuid-from-response" --PROMPT "Now propose the fix"
    ```
    
    ## Passthrough
    
    ```bash
    python scripts/claude_bridge.py mcp list
    python scripts/claude_bridge.py plugin list
    ```
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related