Custom Providers for Claude Phases

August 19, 2026 · View on GitHub

ralphex uses Claude Code as the primary agent for task execution and code reviews. The claude_command and claude_args configuration options allow replacing Claude Code with any CLI tool that produces compatible output — codex, Gemini CLI, local LLMs, or custom scripts. The same provider can also be selected per run with --claude-command and --claude-args.

For codex specifically, use the first-class --codex flag described in the next section when you want codex to be the primary executor. The claude_command wrapper path remains supported for backwards compatibility and for tools without first-class integration (Gemini, Copilot, OpenCode, local LLMs).

Codex executor mode (--codex) — native codex path

The --codex flag is the native way to run the full ralphex pipeline (task execution, both review phases, finalize) through codex. The external review phase is automatically skipped because codex-reviewing-codex is a same-model self-review with weak signal — the cross-model independence between Claude and codex was the original reason that phase existed.

Why this path exists alongside codex-as-claude.sh:

  • ralphex calls the codex CLI directly. No translation layer, no Claude stream-json emulation, no extra jq round-trips.
  • Multi-agent reviews are configured through additive -c flag overrides on the codex command line (-c features.multi_agent=true, -c agents.reviewer.description=...). The overrides layer on top of the user's ~/.codex/config.toml rather than replacing it, so user customizations (model, sandbox, MCP servers) are preserved.
  • Review prompts (review_first.txt, review_second.txt) are shared between claude and codex. The {{agent:<name>}} expander in pkg/processor/prompts.go reads cfg.AppConfig.Executor and emits the executor-appropriate invocation: Use the Task tool ... for claude, spawn_agent(agent='reviewer', task='...') for codex. Under --codex, ralphex additionally prepends a section-level orchestration directive block (the === Codex orchestration directives === preamble) covering spawn_agent fork_context guard and wait_agent dead-agent retry — so users with their own customized review prompts get the directives without touching their prompt files.
  • --pass-claude-md adds -c project_doc_fallback_filenames=["CLAUDE.md"] so codex's native AGENTS.md walk picks up project-level ./CLAUDE.md.

Setup

# one-off
ralphex --codex docs/plans/feature.md

# with project CLAUDE.md passthrough
ralphex --codex --pass-claude-md docs/plans/feature.md

Or persist via config:

# in ~/.config/ralphex/config or .ralphex/config
executor       = codex
pass_claude_md = true

Requirements

--codex requires the codex CLI version 0.130.0 or newer. The mode relies on [features] multi_agent, [agents.<name>] agent registration, and (with --pass-claude-md) project_doc_fallback_filenames — all supported in 0.130.0. Older codex versions silently ignore unknown -c overrides, so a misconfigured run will not error visibly. There is no runtime version check; verify with codex --version.

Mutual exclusion

--codex cannot be combined with --external-only (alias -e), --codex-only (alias -c), or --external-review-tool=<X> where <X> is not none. --pass-claude-md requires the codex executor, enabled either by --codex or executor = codex in config. Each invalid combination fails with a clear error at startup. Config-only conflicts (executor = codex plus external_review_tool = codex in the same config file) are silently resolved by forcing external_review_tool = none and printing a warning to stderr.

Prompt customization

review_first.txt and review_second.txt are shared between claude and codex executors. A user's customized ~/.config/ralphex/prompts/review_first.txt applies under both --codex and default claude. The {{agent:<name>}} expansion within those prompts switches syntax per executor (Task tool for claude, spawn_agent for codex), so the same prompt body works for both.

Under --codex ralphex automatically prepends a section-level orchestration directive block (covering spawn_agent fork_context guard and wait_agent dead-agent retry) at runtime — you do NOT need to put those directives in your customized prompt files. The block is generated by prependCodexReviewGuidance in pkg/processor/prompts.go and only fires when cfg.isCodexExecutor() is true.

User-level CLAUDE.md

--pass-claude-md enables project-level ./CLAUDE.md discovery only. For user-level ~/.claude/CLAUDE.md, ralphex never writes to the user's ~/.codex/ directory. At first --codex --pass-claude-md run, if ~/.claude/CLAUDE.md exists and ~/.codex/AGENTS.md does not, ralphex prints a one-time hint suggesting ln -s ~/.claude/CLAUDE.md ~/.codex/AGENTS.md and continues. The user opts in by running the command themselves.

How it works (claude_command wrapper path)

ralphex's ClaudeExecutor runs the configured command and passes the prompt via stdin, then reads stdout as a stream of JSON events. Each line must be a valid JSON object. The executor recognizes these event types:

Event typeFields usedPurpose
content_block_deltadelta.type ("text_delta"), delta.textStreaming text output
resultresult (string or {"output": "..."})End of execution
assistantmessage.content[].textFull message (alternative to streaming)
message_stopmessage.content[].textFinal message (same structure as assistant)

The executor also recognizes message_stop events, but wrapper scripts don't need to emit these — they are internal to Claude Code. The minimum viable wrapper produces content_block_delta events for text and a result event at the end.

Signal detection

ralphex prompts instruct the agent to emit signals like <<<RALPHEX:COMPLETED>>> or <<<RALPHEX:FAILED>>> in its output. These signals must appear in the text content of content_block_delta or result events. The wrapper doesn't need to handle signals — as long as the underlying tool follows the prompt instructions and the text passes through, signals will be detected automatically.

Argument handling

ClaudeExecutor builds the command as:

<claude_command> <claude_args...> [--model <model>] [--effort <level>] --print

--model and --effort are injected when the current phase's plan_model/task_model/review_model config provides them (via model[:effort] syntax). Either, both, or neither may be present. Any matching flag already in claude_args is stripped before injection to avoid duplicates. Wrappers that don't implement these flags will ignore them via the catch-all *) shift ;; pattern.

The prompt is passed via stdin (not as a CLI argument). This avoids the cmd.exe 8191-character command-line limit on Windows, where large prompts (e.g., after variable expansion) can exceed the limit.

When claude_args has a value (default: --dangerously-skip-permissions --output-format stream-json --verbose), those flags are split and passed as arguments. Wrapper scripts should normally ignore unknown Claude flags. If a wrapper cannot tolerate configured/default arguments, use --claude-args= on the command line to explicitly clear them for a single run.

Wrapper scripts should accept the prompt via stdin and also accept -p <prompt> for backward compatibility. Use [[ ! -t 0 ]] to detect non-interactive stdin before reading. Wrapper scripts should also ignore unknown flags gracefully — use a catch-all *) shift ;; in the argument parser.

Per-run provider overrides

Use CLI flags when you want to test or switch providers without editing ~/.config/ralphex/config or .ralphex/config. These flags override config for the current invocation only:

ralphex --claude-command=/path/to/wrapper.sh --external-review-tool=custom --custom-review-script=/path/to/review.sh docs/plans/feature.md

--external-review-tool accepts codex, custom, or none. When custom is selected, --custom-review-script points at the script that receives the external review prompt file path.

Codex wrapper (included compatibility example)

Compatibility path. The wrapper at scripts/codex-as-claude/codex-as-claude.sh is kept for backwards compatibility — existing setups continue to work, but the wrapper carries overhead (JSONL-to-stream-json translation via jq) and uses Claude-flavored review prompts in front of a codex model. The first-class path avoids both.

The wrapper translates codex JSONL events to Claude stream-json format.

Setup

# in ~/.config/ralphex/config or .ralphex/config
claude_command = /path/to/scripts/codex-as-claude/codex-as-claude.sh

For a one-off run without editing config:

ralphex --claude-command=/path/to/scripts/codex-as-claude/codex-as-claude.sh docs/plans/feature.md

Environment variables

VariableDefaultDescription
CODEX_MODEL(codex default)Model to use with codex
CODEX_SANDBOXdanger-full-accessSandbox mode for codex
CODEX_VERBOSE0Set to 1 to include command execution output (file reads, shell commands)

Event translation

The wrapper translates codex JSONL events as follows:

Codex eventClaude event
item.completed + agent_messagecontent_block_delta with the message text
item.completed + command_executionskipped by default (set CODEX_VERBOSE=1 to include)
item.completed + reasoningskipped
item.startedskipped
turn.completedresult (end of execution)
thread.started, turn.startedskipped

Command execution events are skipped by default because codex reads many files on startup (skills, configs) and echoes their full content, producing excessive noise in the progress log. Agent messages contain the meaningful output.

How it works

# codex emits JSONL like:
{"type":"item.completed","item":{"type":"agent_message","text":"fixed the bug"}}

# wrapper translates to:
{"type":"content_block_delta","delta":{"type":"text_delta","text":"fixed the bug\n"}}

The script uses jq for JSON parsing, which is included in ralphex Docker images and available on most systems.

GitHub Copilot CLI wrapper (included example)

The repository includes a wrapper at scripts/copilot-as-claude/copilot-as-claude.sh that keeps ralphex on the existing claude_command / claude_args path by translating GitHub Copilot CLI JSONL events into Claude stream-json output.

Unlike the Gemini wrapper, Copilot already has a native non-interactive JSONL mode. Unlike OpenCode, it also has native permission flags, so the wrapper can lean on Copilot's own autonomy controls instead of inventing a wrapper-specific config layer. The Copilot wrapper mainly handles prompt ingestion from stdin, event translation, review-prompt adaptation, stderr passthrough, and fallback result emission.

Setup

# in ~/.config/ralphex/config or .ralphex/config
claude_command = /path/to/scripts/copilot-as-claude/copilot-as-claude.sh

For a one-off run without editing config:

ralphex --claude-command=/path/to/scripts/copilot-as-claude/copilot-as-claude.sh docs/plans/feature.md

Authentication

Authenticate Copilot using either:

  • copilot login (OAuth device flow with stored credentials)
  • COPILOT_GITHUB_TOKEN
  • GH_TOKEN
  • GITHUB_TOKEN

Copilot checks the token variables in the order above. Fine-grained PATs must include the Copilot Requests permission. Classic PATs (ghp_) are not supported by the Copilot CLI.

Environment variables

VariableDefaultDescription
COPILOT_MODEL(Copilot CLI default)Model to use
COPILOT_GITHUB_TOKENunsetPreferred auth token for automation
GH_TOKENunsetGitHub CLI token fallback
GITHUB_TOKENunsetFinal auth token fallback
GH_HOSTgithub.comAlternate GitHub host for Enterprise Cloud data residency

Why this wrapper uses Copilot JSONL mode

The wrapper runs Copilot with -s --output-format json --stream on so it can consume native JSONL events instead of scraping terminal text. It emits completed assistant messages rather than token deltas to keep ralphex output readable, while still using explicit completion events to map into Claude result output and echoing stderr back into the stream for existing error and limit detection.

Permission model

The wrapper uses Copilot's native autonomy flags: --autopilot --no-ask-user --allow-all.

  • --autopilot enables the multi-step autonomous execution required for unattended task/review phases
  • --no-ask-user prevents Copilot from pausing the run with follow-up questions
  • --allow-all enables tool, path, and URL permissions together, matching ralphex's unattended task/review model

For ralphex plan creation, the wrapper instead uses --autopilot --allow-all and intentionally leaves off --no-ask-user. Plan mode is supposed to surface clarification through <<<RALPHEX:QUESTION>>> signals, so the wrapper avoids the unattended question-suppression path and instructs Copilot to use signal-based questions instead of the native ask_user tool. It intentionally avoids Copilot's native --mode plan, because that tended to re-draft after user acceptance instead of writing the accepted plan and emitting PLAN_READY.

GitHub's programmatic autopilot guidance uses the same core pattern: "Use the --allow-all (or --yolo) option" together with --autopilot, optionally adding --max-autopilot-continues for a safety cap in CI or scripts.

If you need a narrower policy, fork the wrapper and replace --allow-all with explicit --allow-tool, --allow-url, or related permission flags.

How it differs from other included wrappers

WrapperTransportPermissionsCopilot-specific difference
CodexNative JSONLCodex sandbox/env flagsCopilot uses native --autopilot/--allow-all/--no-ask-user for task/review runs, switches to --autopilot --allow-all for plan creation, and adds adapters for Claude Task-tool wording plus signal-based plan questions
OpenCodeNative JSONLMerges OPENCODE_CONFIG_CONTENT with auto-allow permissionsCopilot uses built-in permission flags rather than JSON config merging
GeminiPlain textGemini CLI settings outside the wrapperCopilot streams structured JSONL events, so the wrapper can emit completed assistant messages and terminal events without scraping plain text lines

OpenCode wrapper (included example)

The repository includes a wrapper at scripts/opencode/opencode-as-claude.sh that translates OpenCode JSONL events to Claude stream-json format. It uses jq for JSON parsing and auto-sets permission auto-allow ({"permission":{"*":"allow"}}) for autonomous execution.

Setup

# in ~/.config/ralphex/config or .ralphex/config
claude_command = /path/to/scripts/opencode/opencode-as-claude.sh

Environment variables

VariableDefaultDescription
OPENCODE_MODEL(opencode default)Model in provider/model format, e.g. github-copilot/claude-opus-4.6
OPENCODE_VARIANT(opencode default)Model variant/reasoning effort, e.g. high, medium, or low
OPENCODE_EFFORT(opencode default)Alias for OPENCODE_VARIANT when OPENCODE_VARIANT is unset
OPENCODE_REASONING(opencode default)Alias for OPENCODE_VARIANT when both OPENCODE_VARIANT and OPENCODE_EFFORT are unset
OPENCODE_VERBOSE0Set to 1 to include step start events in output
OPENCODE_CONFIG_CONTENT{"permission":{"*":"allow"}}JSON config merged with auto-allow permissions via jq deep merge

If OPENCODE_CONFIG_CONTENT is already set, the wrapper merges {"permission":{"*":"allow"}} into it, preserving existing settings. Invalid JSON in this variable causes the wrapper to exit with an error.

Event translation

OpenCode eventClaude event
textcontent_block_delta with .part.text
step_finishresult (end of execution)
step_startskipped by default (set OPENCODE_VERBOSE=1 to include)

Text content is passed verbatim — no truncation or escaping — preserving signal strings like <<<RALPHEX:...>>>. Non-JSON lines are passed through for the executor's non-JSON fallback. Stderr is captured and emitted as content_block_delta events after the main stream for error/limit pattern detection.

How it works

# opencode emits JSONL like:
{"type":"text","part":{"text":"fixed the bug\n"}}

# wrapper translates to:
{"type":"content_block_delta","delta":{"type":"text_delta","text":"fixed the bug\n"}}

For review prompts (detected by <<<RALPHEX:REVIEW_DONE>>> in the prompt text), the wrapper prepends adapter instructions telling the model to execute review agent tasks sequentially, since OpenCode does not support parallel sub-agents.

Gemini CLI wrapper (included example)

The repository includes a wrapper at scripts/gemini-as-claude/gemini-as-claude.sh that translates Gemini CLI plain-text output to Claude stream-json format.

Setup

# in ~/.config/ralphex/config or .ralphex/config
claude_command = /path/to/scripts/gemini-as-claude/gemini-as-claude.sh

Environment variables

VariableDefaultDescription
GEMINI_MODEL(gemini default)Model to use with Gemini CLI

How it works

Since Gemini outputs plain text, the script simply wraps each line in a content_block_delta JSON event.

# gemini emits text like:
fixed the bug

# wrapper translates to:
{"type":"content_block_delta","delta":{"type":"text_delta","text":"fixed the bug\n"}}

Antigravity (agy) CLI wrapper (included example)

The repository includes a wrapper at scripts/agy-as-claude/agy-as-claude.sh that translates the agy (Antigravity) CLI plain-text output to Claude stream-json format.

Compatibility

Tested with agy 1.0.2. The wrapper depends on three agy flags being available:

  • --dangerously-skip-permissions — auto-approve tool/command permissions for unattended runs
  • --print-timeout — print mode timeout (raises the agy default of 5m)
  • -p / --print / --prompt — non-interactive single-prompt mode

If your agy build is missing or renames any of these flags, the wrapper will not work as a Claude replacement.

The agy CLI in this version does not expose a --model flag, so model selection is not surfaced via an AGY_MODEL env var. Configure the model through agy's own configuration if it supports doing so.

Setup

# in ~/.config/ralphex/config or .ralphex/config
claude_command = /path/to/scripts/agy-as-claude/agy-as-claude.sh

Unattended execution

The wrapper invokes agy with --dangerously-skip-permissions to auto-approve tool and command permissions, ensuring the task and review phases can run autonomously without prompts.

Environment variables

VariableDefaultDescription
AGY_PRINT_TIMEOUT2hPrint mode timeout passed to agy. The agy CLI defaults to 5m which is shorter than typical ralphex task/review sessions. Override if you need a different limit.

Environment isolation

To prevent deadlocks when running agy as a sub-process within an active Antigravity agent process, the wrapper unsets every ANTIGRAVITY_* environment variable before calling agy (prefix-wide cleanup via unset ${!ANTIGRAVITY_@}, not a fixed list). This is intentional — it survives Antigravity adding new ANTIGRAVITY_* variables in future versions without requiring wrapper updates. Variables currently known to cause nested-agent issues include:

  • ANTIGRAVITY_AGENT
  • ANTIGRAVITY_TRAJECTORY_ID
  • ANTIGRAVITY_LS_ADDRESS
  • ANTIGRAVITY_CSRF_TOKEN
  • ANTIGRAVITY_PROJECT_ID

If you set custom ANTIGRAVITY_* variables to influence agy behavior and need them inside the wrapper, the prefix-wide cleanup will strip them too — set them inside the wrapper instead, after the unset, or pass them via flags.

How it works

Since agy outputs plain text when run non-interactively, the script wraps each line in a content_block_delta JSON event.

# agy emits text like:
fixed the bug

# wrapper translates to:
{"type":"content_block_delta","delta":{"type":"text_delta","text":"fixed the bug\n"}}

For review prompts (detected by <<<RALPHEX:REVIEW_DONE>>> in the prompt text), the wrapper prepends sequential instructions to run the review flow sequentially rather than attempting parallel execution.

pi CLI wrapper (included example)

The repository includes a wrapper at scripts/pi-as-claude/pi-as-claude.sh that translates pi's --mode json JSONL event stream to Claude stream-json format. Like the Codex and Copilot wrappers, it uses jq for JSON parsing.

The wrapper runs pi with --mode json --print and passes the prompt on stdin (avoiding the per-arg command-line length cap), so pi streams structured JSONL events that the wrapper maps into Claude content_block_delta / result output.

Setup

# in ~/.config/ralphex/config or .ralphex/config
claude_command = /path/to/scripts/pi-as-claude/pi-as-claude.sh

For a one-off run without editing config:

ralphex --claude-command=/path/to/scripts/pi-as-claude/pi-as-claude.sh docs/plans/feature.md

Environment variables

VariableDefaultDescription
PI_PROVIDER(pi default: google)Provider passed as --provider when set
PI_MODEL(pi default)Model used when ralphex does not append a --model flag
PI_THINKING(pi default)Thinking level used when ralphex does not append an --effort flag
PI_VERBOSE0Set to 1 to include tool execution events in the stream
PI_EXTRA_ARGS(none)Extra flags appended verbatim to the pi invocation (word-split on whitespace); e.g. --nolo-mode full to auto-approve tools in non-interactive runs

Thinking / effort mapping

ralphex appends --model <m> / --effort <e> per phase. The wrapper forwards --model to pi's --model and maps --effort to pi's --thinking:

ralphex effortpi thinking
off, minimal, low, medium, high, xhigh, maxpassed through verbatim

pi does not fail on a level it cannot honor, so an effort ralphex forwards is not necessarily the one that runs. max needs pi 0.80.6 or newer: older releases print a warning and run at their own default level instead. Any pi version clamps the level to what the selected model exposes. The wrapper replays pi's stderr after the run ends, so that warning appears in the log only once the work is done.

Event translation

The wrapper translates pi JSONL events as follows:

pi eventClaude event
message_update + assistantMessageEvent.type == "text_delta"buffered; each complete line is flushed as one content_block_delta
tool_execution_start / tool_execution_update / tool_execution_endempty keepalive delta by default (set PI_VERBOSE=1 to include [tool] lines)
session header, queue_update, compaction_*, auto_retry_*empty keepalive delta
turn_end / agent_endflush remaining buffer, then result (end of turn)

pi streams assistant text as token-level deltas (e.g. "The", " quick", " brown"), so the wrapper buffers deltas and emits only complete lines. Emitting one event per token would garble the log with a newline after every token and, more importantly, split <<<RALPHEX:...>>> signals across blocks — the executor's per-block signal detection only matches a signal that lands intact in a single content_block_delta.

Suppressed events are translated to empty text deltas rather than dropped: ralphex's idle_timeout resets on every line of wrapper output, so a long tool execution that produced no output at all would otherwise kill a healthy session. The executor ignores empty text, so keepalives never appear in the progress log.

A fallback {"type":"result","result":""} is always emitted, covering pi exiting without a turn_end/agent_end event. Stderr is captured and emitted as content_block_delta events after the main stream for error/limit pattern detection, and pi's exit code is preserved. Any literal <<<RALPHEX: token on stderr is neutralized first (rewritten to <<< RALPHEX: with an inserted space), so a stray signal token echoed in pi diagnostics cannot be mistaken for a real completion signal — rate-limit and API Error: phrases pass through verbatim for error/limit detection.

How it works

# pi emits token-level JSONL deltas like:
{"type":"message_update","assistantMessageEvent":{"type":"text_delta","delta":"fixed the"}}
{"type":"message_update","assistantMessageEvent":{"type":"text_delta","delta":" bug"}}
{"type":"turn_end"}

# wrapper buffers tokens and flushes the complete line:
{"type":"content_block_delta","delta":{"type":"text_delta","text":"fixed the bug\n"}}

For review prompts (detected by <<<RALPHEX:REVIEW_DONE>>> in the prompt text), the wrapper prepends adapter instructions telling the model to execute review agent tasks sequentially using pi's read/bash/edit/write tools, since pi exposes no parallel sub-agents.

The wrapper covers task and review phases only. Plan creation mode (ralphex --plan) has no pi-specific adapter for QUESTION/PLAN_DRAFT handling (unlike the Copilot wrapper) and is untested with pi.

Writing your own wrapper

A wrapper script must:

  1. Read the prompt from stdin (ralphex pipes it to avoid Windows command-line length limits)
  2. Also accept -p <prompt> as a fallback for backward compatibility
  3. Ignore other flags gracefully
  4. Stream JSON events to stdout, one per line
  5. Exit with code 0 on success
  6. Optionally re-emit the child's stderr as content_block_delta events so ralphex error/limit pattern detection works — neutralize any literal <<<RALPHEX: token first (e.g. insert a space) so stray stderr text cannot be mistaken for a completion signal

Minimal template

#!/usr/bin/env bash
set -euo pipefail

# extract prompt from -p argument (backward compat) or stdin
prompt=""
while [[ $# -gt 0 ]]; do
    case "\$1" in
        -p) prompt="${2:-}"; shift; shift 2>/dev/null || true ;;
        *)  shift ;; # ignore unknown flags
    esac
done

if [[ -z "$prompt" ]]; then
    # fall back to stdin: ralphex passes prompt via pipe to avoid Windows 8191-char cmd limit.
    # only read when stdin is not a terminal to avoid blocking interactive invocations.
    if [[ ! -t 0 ]]; then
        prompt=$(cat)
    fi
fi

if [[ -z "$prompt" ]]; then
    echo "error: no prompt provided (expected -p flag or stdin)" >&2
    exit 1
fi

# call your tool and translate output to claude stream-json.
# each text chunk should be emitted as:
#   {"type":"content_block_delta","delta":{"type":"text_delta","text":"..."}}
#
# end with:
#   {"type":"result","result":""}

# example: pipe tool output line by line
your-tool --prompt "$prompt" | while IFS= read -r line; do
    jq -cn --arg text "$line" \
        '{type: "content_block_delta", delta: {type: "text_delta", text: ($text + "\n")}}'
done

echo '{"type":"result","result":""}'

Gemini CLI example

#!/usr/bin/env bash
set -euo pipefail

prompt=""
while [[ $# -gt 0 ]]; do
    case "\$1" in
        -p) prompt="\$2"; shift 2 ;;
        *)  shift ;;
    esac
done

if [[ -z "$prompt" ]] && [[ ! -t 0 ]]; then prompt=$(cat); fi
[[ -z "$prompt" ]] && exit 1

# gemini outputs plain text; wrap each line as a stream event
gemini -p "$prompt" 2>/dev/null | while IFS= read -r line; do
    jq -cn --arg text "$line" \
        '{type: "content_block_delta", delta: {type: "text_delta", text: ($text + "\n")}}'
done

echo '{"type":"result","result":""}'

Local LLM (ollama) example

#!/usr/bin/env bash
set -euo pipefail

prompt=""
while [[ $# -gt 0 ]]; do
    case "\$1" in
        -p) prompt="\$2"; shift 2 ;;
        *)  shift ;;
    esac
done

if [[ -z "$prompt" ]] && [[ ! -t 0 ]]; then prompt=$(cat); fi
[[ -z "$prompt" ]] && exit 1

OLLAMA_MODEL="${OLLAMA_MODEL:-llama3}"

# ollama with JSON streaming
ollama run "$OLLAMA_MODEL" "$prompt" 2>/dev/null | while IFS= read -r line; do
    jq -cn --arg text "$line" \
        '{type: "content_block_delta", delta: {type: "text_delta", text: ($text + "\n")}}'
done

echo '{"type":"result","result":""}'

OpenRouter API example

#!/usr/bin/env bash
set -euo pipefail

prompt=""
while [[ $# -gt 0 ]]; do
    case "\$1" in
        -p) prompt="\$2"; shift 2 ;;
        *)  shift ;;
    esac
done

if [[ -z "$prompt" ]] && [[ ! -t 0 ]]; then prompt=$(cat); fi
[[ -z "$prompt" ]] && exit 1

OPENROUTER_MODEL="${OPENROUTER_MODEL:-anthropic/claude-sonnet-4}"

response=$(curl -s https://openrouter.ai/api/v1/chat/completions \
    -H "Authorization: Bearer $OPENROUTER_API_KEY" \
    -H "Content-Type: application/json" \
    -d "$(jq -cn --arg model "$OPENROUTER_MODEL" --arg prompt "$prompt" '{
        model: $model,
        messages: [{role: "user", content: $prompt}]
    }')")

text=$(echo "$response" | jq -r '.choices[0].message.content // empty')
if [[ -n "$text" ]]; then
    jq -cn --arg text "$text" \
        '{type: "content_block_delta", delta: {type: "text_delta", text: ($text + "\n")}}'
fi

echo '{"type":"result","result":""}'

Limitations and considerations

Signal emission: the underlying tool must follow ralphex prompt instructions to emit <<<RALPHEX:...>>> signals. Most capable models (GPT-4+, Claude, Gemini Pro) handle this reliably. Smaller/local models may not follow signal instructions consistently, which will cause ralphex to retry or timeout.

Tool use: Claude Code natively supports file editing, command execution, and other tools. Alternative providers typically only output text — they cannot directly edit files or run commands. This means they work best for review phases (where the output is analyzed by Claude for fixing) rather than task execution phases (where the agent needs to write code and run tests).

Streaming: the wrapper should emit events as they become available, not buffer the entire response. This allows ralphex to show real-time progress. The codex wrapper achieves this via the while IFS= read -r line pattern.

Error handling: if the underlying tool fails, the wrapper should either exit with a non-zero code or emit an error in a result event. ralphex's ClaudeExecutor handles both cases.

Docker: when running in Docker, ensure the wrapper script and its dependencies (jq, curl, etc.) are available inside the container. The ralphex base image includes jq. Mount custom scripts as read-only volumes.

Troubleshooting

Empty output / no events:

  • Check that the tool is actually producing output: run the wrapper manually with echo "say hello" | your-wrapper
  • Verify stderr is redirected (add 2>/dev/null for the underlying tool)
  • Ensure jq is installed and accessible

Signals not detected:

  • The model must include <<<RALPHEX:COMPLETED>>> or <<<RALPHEX:FAILED>>> in its text output
  • Check that the prompt is passed through correctly (not truncated or escaped)
  • Test manually: run the wrapper with a prompt that includes signal instructions

JSON parsing errors:

  • Each line must be a complete, valid JSON object
  • No trailing commas, no multi-line JSON objects
  • Test with: echo "test" | your-wrapper | jq . (each line should parse)

Timeout / stuck:

  • ralphex supports an optional per-session timeout via --session-timeout flag or session_timeout config option (e.g., 30m, 1h). In default Claude executor mode it applies to Claude calls only; under --codex it applies to every executor call. External codex/custom review in Claude mode is not affected.
  • ralphex also supports --idle-timeout flag or idle_timeout config option (e.g., 5m). Unlike session timeout (fixed wall-clock limit), idle timeout resets on each output line and fires only when the session goes silent. It applies to the Claude executor in default mode and to every executor call under --codex; external codex review in default-claude mode is not affected. Custom review is not affected.
  • Check if the underlying tool has its own timeout settings
  • For codex: adjust CODEX_SANDBOX if the sandbox is blocking operations