cd /news/developer-tools/cross-agent-cli-for-other-agents-to-… · home topics developer-tools article
[ARTICLE · art-89800] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Cross-agent CLI for other agents to use; currently supports claude, codex, cursor agent, copilot, and grok.

A developer has created agent_cli, a cross-agent command-line tool that unifies five AI coding agents—Claude, Codex, Cursor, Copilot, and Grok—behind a single interface. The script supports both read-only review and write-capable edit modes, with explicit session management and per-agent model and effort defaults. It aims to simplify cross-agent workflows by centralizing transport quirks and providing a consistent signature for prompts, sessions, and modes.

read17 min views2 publishedJul 22, 2026

| #!/usr/bin/env bash | | | # | | | # agent_cli — one front door for cross-agent work: read-only review/research | | | # OR write-capable edit/refactor. | | | # | | | # Generalizes agent_review (read-only only) by adding an optional trailing | | | # mode arg. review (default) keeps every agent read-only — identical to | | | # agent_review. edit flips each CLI into its write-capable mode so a worker | | | # can apply targeted refactors/edits. Append edit to allow writes. | | | # | | | # Session continuity is explicit: the first arg is either new (fresh | | | # session) or a session ID from a previous run (resume with full context). | | | # There is no implicit "continue last session" — you always say which. | | | # | | | # Wraps the five CLI agents (copilot, codex, claude, cursor, grok) behind a single | | | # signature so one doc describes one wrapper instead of five. This script owns the | | | # per-CLI transport quirks (read-only vs write mode, stdin redirect, effort | | | # flag mapping, default models). When a CLI's API changes, fix it HERE. | | | # | | | # Usage: | | | # Tools/agent_cli <session ID | new> <agent> <model|default> <effort|default> "<prompt>" [mode] | | | # | | | # <session> new starts a fresh session; a session ID resumes that session. | | | # Every run prints session: <id> on stderr for later resumption. | | | # IDs are per-agent (claude/copilot/grok UUIDs, codex thread ids, | | | # cursor chat ids) — resume with the same agent that minted the id. | | | # <agent> copilot | codex | claude | cursor | grok | | | # <model> a model id for that agent, or default (see DEFAULT_MODEL_* below) | | | # <effort> low | medium | high | none | default (mapped per agent) | |

| # <prompt> the prompt; pass `-` to read it from stdin | |
| # [mode] review (default, read-only) | edit (write-capable) | |

| # | | | # Examples: | | | # Tools/agent_cli new copilot default high "Review the uncommitted diff." | | | # Tools/agent_cli new codex gpt-5.6-sol high "Review Scripts/TreeRegion.gd for perf." | | | # Tools/agent_cli new claude default default "Refactor Scripts/Foo.gd: extract helper." edit | | | # Tools/agent_cli 0198c2… claude default default "Now run the tests you suggested." edit | | | # git diff main...HEAD | Tools/agent_cli new claude default high - | | | # | | | # review mode runs READ-ONLY (no edits); edit mode allows writes within the | | | # repo. The repo is the current git toplevel (override with REPO=/path). | | | # List models with: Tools/agent_cli <agent> --list-models | | | set -euo pipefail | | | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | | | # shellcheck source=/dev/null | | | source "$SCRIPT_DIR/colors.sh" | | | REPO="${REPO:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" | | | # Per-agent defaults. Selection rationale lives in AI_MODEL_COMPARISON.md. | | | DEFAULT_MODEL_copilot="auto" # Auto grants more token usage than pinning a model. Free tokens. | |

| DEFAULT_MODEL_codex="gpt-5.6-terra" | |
| DEFAULT_MODEL_claude="claude-opus-4-8" | |

| DEFAULT_MODEL_cursor="composer-2.5" | | | DEFAULT_MODEL_grok="grok-4.5" | | | DEFAULT_EFFORT_copilot="low" # Informational for auto, which rejects explicit effort. | | | DEFAULT_EFFORT_codex="high" # Benchmark-backed routine review/final-edit setting. | | | DEFAULT_EFFORT_claude="medium" # Normal work; use claude-fable-5 low for very hard tasks. | | | DEFAULT_EFFORT_cursor="low" # Informational; Cursor effort is encoded in model ids. | | | DEFAULT_EFFORT_grok="high" # Unbenchmarked secondary perspective; use high effort. | | | usage() { | | | cat <<'EOF' | | | Usage: Tools/agent_cli <session ID | new> <agent> <model|default> <effort|default> "<prompt>" [mode] | | | session new for a fresh session, or a session ID to resume (printed as | | | session: <id> on stderr by every run; IDs are per-agent) | | | agent copilot | codex | claude | cursor | grok | | | model a model id, or default | | | effort low | medium | high | none | default | | | prompt the prompt; - reads from stdin | | | mode review (default, read-only) | edit (write-capable) | | | review mode runs read-only; edit mode allows writes within the repo. | | | Auth/health checks: | | | Tools/agent_cli <agent> --check-auth # Test auth for one agent | | | Tools/agent_cli --doctor # Test all five agents | | | List models: Tools/agent_cli <agent> --list-models | | | EOF | | | } | |

| die() { echo -e "${RED}agent_cli: $*${NC}" >&2; exit 1; } | |
| note() { echo -e "${DKGRAY}» $*${NC}" >&2; } | |

| REVIEW_COMPLETION_CONTRACT='Review completion contract: | | | - After all tool use, always return a final answer on stdout. Do not stop after a preamble, progress update, or tool result. | | | - State the findings with severity and file/line references. If there are no findings, state PASS explicitly. | | | - Do not use /tmp or another file as the only report channel; the caller only receives your final stdout. | | | - End with exactly one of these lines: | | | REVIEW_COMPLETE: PASS | | | REVIEW_COMPLETE: FINDINGS' | |

| finish_result() { | |
| local result="$1" status="$2" | |

| printf '%s\n' "$result" | |

| [ "$status" -eq 0 ] || exit "$status" | |
| if [ "$MODE" = "review" ] && [ "${AGENT_CLI_SKIP_REVIEW_COMPLETION:-0}" != "1" ]; then | |

| local last_line | | | last_line="$(awk 'NF { line = $0 } END { print line }' <<<"$result")" | | | case "$last_line" in | | | "REVIEW_COMPLETE: PASS"|"REVIEW_COMPLETE: FINDINGS") ;; | | | *) | | | echo "agent_cli: reviewer exited 0 without the required final verdict; review is incomplete, not PASS" >&2 | | | exit 2 | | | ;; | | | esac | | | fi | | | } | |

| [ $# -ge 1 ] || { usage >&2; exit 1; } | |
| # Flag forms keep their original shapes (<agent> --list-models, | |
| # <agent> --check-auth, --doctor); the run form is session-first. | |
| if [ "${2:-}" = "--list-models" ] || [ "${2:-}" = "models" ] || [ "${2:-}" = "--check-auth" ] \ | |
| || [ "$1" = "--doctor" ] || [ "$1" = "doctor" ]; then | |
| AGENT="$1"; shift | |

| else | | | SESSION="$1"; shift | | | case "$SESSION" in | | | copilot|codex|claude|cursor|grok) | |

| die "first arg is now <session ID | new> (got agent '$SESSION'); use \`new\` for a fresh session" ;; | |
| "") die "empty session arg (expected a session ID or \`new\`)" ;; | |

| esac | |

| [ $# -ge 1 ] || { usage >&2; die "need <agent>"; } | |
| AGENT="$1"; shift | |

| fi | |

| # --list-models shortcut | |
| if [ "${1:-}" = "--list-models" ] || [ "${1:-}" = "models" ]; then | |

| case "$AGENT" in | |

| cursor) exec cursor-agent --list-models ;; | |
| codex) exec codex --help ;; # codex enumerates via -m; no list subcommand | |
| copilot) die "copilot has no headless model list; run \`copilot\` then /model. Known good: gpt-5.4, claude-sonnet-4-6, auto" ;; | |
| claude) die "claude model ids: claude-fable-5, claude-opus-4-8, claude-sonnet-5, claude-haiku-4-5-20251001" ;; | |
| grok) exec grok models ;; | |
| *) die "unknown agent '$AGENT'" ;; | |

| esac | | | fi | | | # Auth check functions | | | check_agent_auth() { | | | local agent="$1" | |

| local model; v="DEFAULT_MODEL_$agent"; model="${!v:-}" | |
| [ -z "$model" ] && die "unknown agent '$agent'" | |
| note "checking $agent auth (model: $model)..." | |

| local out exit_code=0 | |

| out="$(AGENT_CLI_SKIP_REVIEW_COMPLETION=1 "$0" new "$agent" "$model" none "Reply with exactly: OK" review 2>&1)" || exit_code=$? | |
| if grep -qi "OK" <<<"$out"; then | |
| echo "${GREEN}✓${NC} $agent: authenticated" >&2 | |

| return 0 | | | elif grep -qi "exceeded your monthly quota|quota exceeded|rate limit" <<<"$out"; then | | | echo "${RED}✗${NC} $agent: authenticated but out of quota / rate limited" >&2 | | | echo " Wait for the quota window to reset or use another agent" >&2 | | | return 1 | | | else | | | echo "${RED}✗${NC} $agent: authentication failed or no response" >&2 | | | echo " Run the $agent CLI interactively to re-authenticate" >&2 | | | [ "$exit_code" -ne 0 ] && echo " Exit code: $exit_code" >&2 | | | return 1 | | | fi | | | } | |

| # --check-auth shortcut | |
| if [ "${1:-}" = "--check-auth" ]; then | |

| check_agent_auth "$AGENT" || exit 1 | | | exit 0 | | | fi | |

| # --doctor checks all agents | |
| if [ "$AGENT" = "--doctor" ] || [ "$AGENT" = "doctor" ]; then | |

| failures=0 | | | for ag in copilot codex claude cursor grok; do | | | check_agent_auth "$ag" || failures=$((failures + 1)) | | | done | |

| [ $failures -eq 0 ] && { echo "${GREEN}All agents authenticated${NC}" >&2; exit 0; } | |
| echo "${RED}$failures agent(s) failed authentication${NC}" >&2 | |

| exit 1 | | | fi | |

| [ $# -ge 3 ] || { usage >&2; die "need <model> <effort> <prompt>"; } | |
| MODEL="$1"; EFFORT="$2"; PROMPT="$3"; MODE="${4:-review}" | |
| [ "$MODEL" = "default" ] && { v="DEFAULT_MODEL_$AGENT"; MODEL="${!v:-}"; [ -n "$MODEL" ] || die "unknown agent '$AGENT'"; } | |
| [ "$EFFORT" = "default" ] && { v="DEFAULT_EFFORT_$AGENT"; EFFORT="${!v:-}"; [ -n "$EFFORT" ] || die "no default effort for '$AGENT'"; } | |
| [ "$PROMPT" = "-" ] && PROMPT="$(cat)" | |
| [ -n "$PROMPT" ] || die "empty prompt" | |

| case "$MODE" in | |

| review|edit) ;; | |
| *) die "unknown mode '$MODE' (expected review | edit)" ;; | |

| esac | | | if [ "$MODE" = "review" ] && [ "${AGENT_CLI_SKIP_REVIEW_COMPLETION:-0}" != "1" ]; then | | | PROMPT="$(printf '%s\n\n%s' "$PROMPT" "$REVIEW_COMPLETION_CONTRACT")" | | | fi | | | case "$AGENT" in | | | copilot) | | | command -v copilot >/dev/null 2>&1 || die "copilot CLI not found" | |

| # review: --plan = read-only/planning (no edits). edit: drop --plan so | |
| # the agent can write. --allow-all-tools suppresses permission prompts on | |
| # tools (required non-interactive). Some models (auto, *-4.5) reject | |

| # --effort, so retry without it on error. | |

| eff=() | |
| [ "$EFFORT" != "none" ] && [ "$MODEL" != "auto" ] && eff=(--effort "$EFFORT") | |
| plan=(--plan) | |
| [ "$MODE" = "edit" ] && plan=() | |

| # --session-id both names a new session and resumes an existing one. | | | sess_id="$SESSION" | |

| [ "$sess_id" = "new" ] && sess_id="$(uuidgen | tr '[:upper:]' '[:lower:]')" | |
| note "copilot --model $MODEL ${eff[*]:-} ${plan[*]:-} ($MODE)" | |

| note "session: $sess_id" | | | status=0 | | | out="$(copilot -p "$PROMPT" --model "$MODEL" --session-id "$sess_id" ${eff[@]+"${eff[@]}"} ${plan[@]+"${plan[@]}"} --allow-all-tools --no-color -C "$REPO" </dev/null 2>&1)" || status=$? | | | if grep -q "does not support reasoning effort" <<<"$out"; then | | | note "model rejects --effort; retrying without it" | | | status=0 | | | out="$(copilot -p "$PROMPT" --model "$MODEL" --session-id "$sess_id" ${plan[@]+"${plan[@]}"} --allow-all-tools --no-color -C "$REPO" </dev/null 2>&1)" || status=$? | | | fi | | | finish_result "$out" "$status" | | | ;; | | | codex) | | | command -v codex >/dev/null 2>&1 || die "codex CLI not found" | |

| sandbox="read-only" | |
| [ "$MODE" = "edit" ] && sandbox="workspace-write" | |

| status=0 | |

| if [ "$SESSION" = "new" ]; then | |
| note "codex exec -m $MODEL model_reasoning_effort=$EFFORT -s $sandbox ($MODE)" | |
| raw="$(codex exec --strict-config --json -m "$MODEL" -c model_reasoning_effort="$EFFORT" \ | |
| -s "$sandbox" -C "$REPO" "$PROMPT" </dev/null)" || status=$? | |

| else | | | # codex exec resume has no -s/-C flags; sandbox goes through config | | | # and the working directory through a subshell cd. | |

| note "codex exec resume $SESSION -m $MODEL model_reasoning_effort=$EFFORT sandbox_mode=$sandbox ($MODE)" | |
| raw="$( (cd "$REPO" && codex exec resume --strict-config --json -m "$MODEL" \ | |
| -c model_reasoning_effort="$EFFORT" -c sandbox_mode="$sandbox" \ | |
| "$SESSION" "$PROMPT") </dev/null)" || status=$? | |

| fi | | | result="$(printf '%s\n' "$raw" | python3 -c ' | | | import sys, json | | | message = "" | | | thread_id = "" | | | for line in sys.stdin: | | | line = line.strip() | | | if not line: continue | | | try: obj = json.loads(line) | | | except Exception: continue | |

| if obj.get("type") == "thread.started" and obj.get("thread_id"): | |
| thread_id = obj["thread_id"] | |
| item = obj.get("item", {}) | |
| if obj.get("type") == "item.completed" and item.get("type") == "agent_message" and item.get("text"): | |
| message = item["text"] | |

| if thread_id: | |

| print(f"session: {thread_id}", file=sys.stderr) | |
| print(message if message else "(codex returned no result text)")')" | |

| finish_result "$result" "$status" | | | ;; | | | claude) | | | command -v claude >/dev/null 2>&1 || die "claude CLI not found" | | | # Headless Claude can silently stall when permission prompts cannot be | | | # displayed. Use full-access mode in this trusted repo; review-only behavior | | | # is enforced by the prompt contract, not by Claude's plan-mode permissions. | | | # Sessions persist so they can be resumed by ID; the explicit --session-id | | | # still prevents the desktop-managed binary from silently resuming an | | | # unrelated prior task. | |

| if [ "$SESSION" = "new" ]; then | |
| claude_session_id="$(uuidgen | tr '[:upper:]' '[:lower:]')" | |
| sess=(--session-id "$claude_session_id") | |

| else | | | claude_session_id="$SESSION" | | | sess=(--resume "$SESSION") | | | fi | | | cprompt="$PROMPT" | |

| [ "$MODE" = "review" ] && cprompt="$(printf 'Review-only: do not edit files.\n\n%s' "$PROMPT")" | |
| note "claude -p --model $MODEL --effort $EFFORT --permission-mode bypassPermissions ($MODE)" | |

| note "session: $claude_session_id" | | | status=0 | | | raw="$(cd "$REPO" && printf '%s' "$cprompt" | claude -p \ | |

| --model "$MODEL" --effort "$EFFORT" \ | |
| "${sess[@]}" \ | |
| --permission-mode bypassPermissions --output-format json)" || status=$? | |
| result="$(printf '%s\n' "$raw" | python3 -c ' | |

| import json, sys | | | raw = sys.stdin.read() | | | try: | |

| obj = json.loads(raw) | |
| print(obj.get("result") or "(claude returned no result text)") | |

| except Exception: | | | print(raw.strip() or "(claude returned no result text)")')" | | | finish_result "$result" "$status" | | | ;; | | | cursor) | |

| command -v cursor-agent >/dev/null 2>&1 || die "cursor-agent CLI not found" | |
| # Cursor bakes effort into model ids (e.g. gpt-5.5-high); EFFORT is | |
| # informational only. review: --mode plan blocks edits. edit: cursor's | |
| # write/agent mode is the default, so we OMIT --mode (its only --mode | |

| # choices are plan|ask; there is no agent value). Headless -p cannot | | | # show tool-permission prompts, so --force --trust are required or it | | | # hangs and exits empty. stream-json reliably emits the final result text | | | # (text format drops it for agentic runs), so we stream the final result. | |

| cursor_review_mode="${AGENT_CLI_CURSOR_REVIEW_MODE:-plan}" | |
| case "$cursor_review_mode" in plan|ask) ;; *) die "AGENT_CLI_CURSOR_REVIEW_MODE must be plan or ask" ;; esac | |
| cmode=(--mode "$cursor_review_mode"); cprompt="Review-only: do not edit any files. $PROMPT" | |
| [ "$MODE" = "edit" ] && { cmode=(); cprompt="$PROMPT"; } | |

| # A new run pre-mints a chat id via create-chat so the id is known before | | | # dispatch; both branches then attach with --resume. Re-prefixing | | | # Review-only: on a resumed session makes Composer flake (empty result | | | # or degenerate repetition), so the prefix is first-message-only; --mode | | | # plan remains the hard read-only gate on every review call. | | | cursor_chat_id="$SESSION" | |

| if [ "$cursor_chat_id" = "new" ]; then | |
| cursor_chat_id="$(cursor-agent create-chat)" || die "cursor-agent create-chat failed" | |
| [ -n "$cursor_chat_id" ] || die "cursor-agent create-chat returned no chat id" | |

| else | | | cprompt="$PROMPT" | | | fi | | | note "cursor-agent -p --output-format stream-json ${cmode[*]:-} --force --trust --model $MODEL ($MODE; effort '$EFFORT' n/a)" | | | note "session: $cursor_chat_id" | | | status=0 | |

| raw="$(cursor-agent -p --output-format stream-json ${cmode[@]+"${cmode[@]}"} --force --trust \ | |
| --workspace "$REPO" --model "$MODEL" --resume "$cursor_chat_id" \ | |
| "$cprompt")" || status=$? | |
| result="$(printf '%s\n' "$raw" | python3 -c ' | |

| import sys, json | | | assistant = "" | | | result = "" | | | for line in sys.stdin: | | | line = line.strip() | | | if not line: continue | | | try: obj = json.loads(line) | | | except Exception: continue | |

| if obj.get("type") == "assistant": | |
| content = obj.get("message", {}).get("content", []) | |
| text = "".join(item.get("text", "") for item in content if item.get("type") == "text") | |
| if text.strip(): assistant = text | |
| elif obj.get("type") == "result" and obj.get("result"): | |
| result = obj["result"] | |

| print(assistant if assistant else result if result else "(cursor returned no result text)")')" | | | finish_result "$result" "$status" | | | ;; | | | grok) | | | command -v grok >/dev/null 2>&1 || die "grok CLI not found" | | | # Both modes run --permission-mode bypassPermissions: headless grok | | | # cancels the session (stopReason "Cancelled", answer lost) when plan or | | | # dontAsk mode hits a tool approval, and --sandbox read-only does not | | | # actually block writes on this host. Review read-only is therefore | | | # prompt-level (Review-only prefix) like claude, plus --disallowed-tools | | | # to strip the direct edit tools; Bash remains available for searching. | |

| # Effort maps to --reasoning-effort (high|medium|low); omit for `none`. | |
| eff=() | |
| [ "$EFFORT" != "none" ] && eff=(--reasoning-effort "$EFFORT") | |
| gate=(); gprompt="$PROMPT" | |
| if [ "$MODE" = "review" ]; then | |
| gate=(--disallowed-tools "Write,Edit,MultiEdit,NotebookEdit") | |
| gprompt="Review-only: do not edit any files. $PROMPT" | |

| fi | |

| if [ "$SESSION" = "new" ]; then | |
| grok_session_id="$(uuidgen | tr '[:upper:]' '[:lower:]')" | |
| sess=(--session-id "$grok_session_id") | |

| else | | | grok_session_id="$SESSION" | | | sess=(--resume "$SESSION") | | | fi | | | note "grok -p -m $MODEL ${eff[]:-} --permission-mode bypassPermissions ${gate[]:-} ($MODE)" | | | note "session: $grok_session_id" | | | status=0 | |

| raw="$(grok -p "$gprompt" -m "$MODEL" ${eff[@]+"${eff[@]}"} \ | |
| --permission-mode bypassPermissions ${gate[@]+"${gate[@]}"} \ | |
| "${sess[@]}" --cwd "$REPO" --output-format json </dev/null)" || status=$? | |
| result="$(printf '%s\n' "$raw" | python3 -c ' | |

| import json, sys | | | raw = sys.stdin.read() | | | try: | |

| obj = json.loads(raw) | |
| print(obj.get("text") or "(grok returned no result text)") | |

| except Exception: | | | print(raw.strip() or "(grok returned no result text)")')" | | | finish_result "$result" "$status" | | | ;; | | | *) | | | die "unknown agent '$AGENT' (expected copilot | codex | claude | cursor | grok)" | | | ;; | | | esac |

── more in #developer-tools 4 stories · sorted by recency
── more on @claude 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/cross-agent-cli-for-…] indexed:0 read:17min 2026-07-22 ·