
Collaborating With Claude
- 1 installs
- 130 repo stars
- Updated July 11, 2026
- appautomaton/agent-designer
collaborating-with-claude is a Claude skill that drives the Claude Code CLI headlessly through a bridge script so a primary agent can delegate coding tasks and get structured JSON results.
About
This skill lets a calling agent delegate tasks to the Claude Code CLI headlessly through a bridge script (scripts/claude_bridge.py) that wraps claude --print. A developer uses it to get second opinions, propose or review diffs, and run multi-turn analysis while the primary agent stays responsible for verification. It returns structured JSON with session id, cost, and tool telemetry, and gates authority up front via permission-mode and tool flags.
- Delegates tasks to the Claude Code CLI headlessly via a bridge script
- Wraps claude --print, returns structured JSON with cost and telemetry
- Supports multi-turn continuity via SESSION_ID and permission-mode gating
Collaborating With Claude by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,102 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
collaborating-with-claude capabilities & compatibility
- Capabilities
- agent delegation · code review · debugging
- Use cases
- code review · debugging · orchestration
What collaborating-with-claude says it does
Drive Claude Code headlessly as an independent collaborator while the calling agent stays responsible for verification, synthesis, and final user-facing decisions.
The bridge (`scripts/claude_bridge.py`) wraps `claude --print`, streams progress to stderr, returns structured JSON with telemetry, and manages multi-turn continuity via `SESSION_ID`.
npx skills add https://github.com/appautomaton/agent-designer --skill collaborating-with-claudeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 130 |
| Last updated | July 11, 2026 |
| Repository | appautomaton/agent-designer ↗ |
What it does
Delegate prototyping, debugging, or code review to a headless Claude Code CLI session and read back structured JSON results.
Who is it for?
Getting a second-model opinion, diff review, or multi-turn analysis while the primary agent implements.
Skip if: Trivial one-shot edits, tasks needing authoritative cited facts, or anything touching secrets or prod data.
When should I use this skill?
You want to hand a coding subtask to a headless Claude Code session and get JSON back.
What you get
The bridge returns structured JSON with SESSION_ID, cost, and tool telemetry for consistent delegation.
- Structured JSON result with SESSION_ID, cost, and tool telemetry
By the numbers
- Verified on Claude Code 2.1.176
Files
Collaborating with Claude Code
Drive Claude Code headlessly as an independent collaborator while the calling agent stays responsible for verification, synthesis, and final user-facing decisions.
The bridge (scripts/claude_bridge.py) wraps claude --print, streams progress to stderr, returns structured JSON with telemetry, and manages multi-turn continuity via SESSION_ID. Always go through the bridge — don't invoke claude directly — so output parsing and session handling stay consistent.
In Claude Code, run non-trivial calls in the background and watch the stderr progress:
Bash tool call:
command: python3 skills/collaborating-with-claude/scripts/claude_bridge.py --cd "/project" --PROMPT "Analyze auth flow in src/auth/"
run_in_background: truerun_in_background is a host tool parameter, not a shell argument. Use the task-output view to monitor timestamped stderr progress (session, responses, tools, cost) and the final JSON result.
Safety
Default to read-only delegation: --permission-mode plan (analyze, no edits/commands) or --tools "Read,Glob,Grep". Grant writes only deliberately (acceptEdits/auto), preferably in an isolated worktree. Do not hand secrets, private keys, or production data to Claude. Full permission-mode set and the worktree pattern: cli-reference.md, handoff-patterns.md.
Permissions and network (headless)
Headless claude -p cannot prompt: every gated action is denied on the spot and recorded in permission_denials, which the bridge surfaces (verified on 2.1.176). Authority is therefore decided entirely up front via --permission-mode, --tools, and --allowed-tools — get user consent before granting anything beyond read-only.
Network is governed by tool policy, not an OS sandbox: plan mode denies WebFetch/WebSearch too (verified), while an allowed Bash can reach the network freely. Pick the posture per task:
- No network, read-only:
--permission-mode plan, or--tools "Read,Glob,Grep". - Read-only plus targeted web research (verified):
--permission-mode dontAsk --tools "Read,Glob,Grep,WebFetch,WebSearch" --allowed-tools "WebFetch(domain:example.com)" --allowed-tools "WebSearch". - Reads outside
--cdare gated as well — grant extra roots with--add-dir.
When to use / not use
Use for: second opinions on design, edge cases, or test gaps; proposing or reviewing a unified diff; multi-turn analysis while you implement. Skip for: trivial one-shot edits (do them directly); tasks needing authoritative cited facts (Claude may guess); anything touching secrets or prod data.
Quick start
⚠️ Backticks / $VARS in prompts trigger shell expansion — use a single-quoted heredoc, or --prompt-file for large/generated prompts. See shell-quoting.md.
PROMPT="$(cat <<'EOF'
Review src/auth.py around login() and propose fixes.
OUTPUT: Unified Diff Patch ONLY.
EOF
)"
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "." --model sonnet --permission-mode plan --PROMPT "$PROMPT" --output-format stream-jsonFor large or shell-sensitive prompts, write the prompt to a file and pass --prompt-file /tmp/prompt.md (piped via stdin — no argv/quoting limits).
Returns (stdout JSON): { "success": true, "SESSION_ID": "...", "agent_messages": "...", "model": "...", "subtype": "success", "total_cost_usd": 0.03, "usage": {...}, "num_turns": 1 } — plus tools_used / tools_failed / tool_counts / permission_denials / structured_output / is_error when relevant. Check tools_failed and permission_denials before trusting the answer: a denied tool means Claude reasoned without the evidence it asked for. Progress streams to stderr; the bridge exits non-zero on failure.
Multi-turn sessions
Capture SESSION_ID from the first call and pass it back (selectors are mutually exclusive):
# Turn 1
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "." --model sonnet --PROMPT "Analyze the bug in foo()." --output-format stream-json
# Turn 2 — resume by ID (use the same --cd)
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "." --model sonnet --SESSION_ID "<id>" --PROMPT "Propose a fix." --output-format stream-json
# Or resume the most recent session in this directory
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "." --model sonnet --continue --PROMPT "What about edge cases?" --output-format stream-jsonUse stream-json or json output to capture SESSION_ID.
Bridge flags
Core: --PROMPT (or --prompt-file) · --cd (required) · --model (alias haiku/sonnet/opus/fable, or full id) · --output-format (text·json·stream-json, default stream-json).
Sessions (mutually exclusive): --SESSION_ID · --session-id <uuid> · --continue; plus --fork-session, --no-session-persistence.
Permissions: --permission-mode (default·plan·acceptEdits·auto·dontAsk·bypassPermissions) · --tools · --allowed-tools · --disallowed-tools. Footgun: the space in Bash(git diff *) is load-bearing.
Reproducibility & cost: --bare / --safe-mode (skip customizations; --bare needs ANTHROPIC_API_KEY) · --effort (low→max) · --max-budget-usd · --max-turns · --timeout <seconds>.
Context & advanced: --prompt-file · --system-prompt[-file] · --append-system-prompt[-file] · --add-dir · --json-schema · --mcp-config · --settings · --agent/--agents · --return-all-messages · --verbose.
Full semantics in cli-reference.md. Set the host's timeout_ms to 600000 (10 min) when invoking via a command runner.
Tune performance
--model haiku for quick checks, sonnet for routine work, opus or fable for hard tasks; --effort low→max trades depth for speed/cost; --max-budget-usd caps spend. Omit --model to use the CLI default.
Prompting
Quick starters in prompt-template.md; composable XML blocks in prompt-blocks.md; end-to-end recipes in prompt-recipes.md; delegation patterns and principles in patterns.md. In short: point (file:line), don't paste; one objective per run; state the output shape; verify Claude's output before acting.
Verification
- Smoke:
python3 skills/collaborating-with-claude/scripts/claude_bridge.py --help - Syntax:
python3 -m py_compile skills/collaborating-with-claude/scripts/claude_bridge.py - Session: run a prompt with
--output-format stream-json; confirm JSON hassuccess: true, aSESSION_ID, and telemetry (subtype/total_cost_usd/usage/num_turns); failures exit non-zero. - Ensure Claude is logged in (
claudethen/login), or setANTHROPIC_API_KEY(required for--bare).
Collaboration State Capsule
Keep this updated across turns (referenced by handoff-patterns.md):
[Claude Capsule] Goal: | SID: | Model: | PermMode: | Files: | Last: | Next:References
- prompt-template.md — quick plain-text starters
- prompt-blocks.md — composable XML blocks
- prompt-recipes.md — end-to-end templates
- prompt-antipatterns.md — common mistakes
- patterns.md — when to delegate + prompt patterns
- handoff-patterns.md — read-only / worktree / synthesis
- parallel.md — parallel runs and worktree isolation
- cli-reference.md — verified Claude CLI flags + event schema
- shell-quoting.md — safe heredoc prompts
Claude Prompt Template (Token-Efficient)
Prefer model aliases (--model sonnet for routine work, --model opus for harder tasks); omit --model to use the CLI default. For complex or high-stakes work, upgrade to the XML blocks in ../references/prompt-blocks.md and recipes in ../references/prompt-recipes.md.
Analysis / Plan (read-only)
Task:
- <what to analyze>
Repo pointers:
- <file paths + approximate line numbers>
Constraints:
- Keep it concise and actionable.
- Reference files/lines instead of pasting code.
Output:
- Bullet list of findings and a proposed plan.Patch (Unified Diff only)
Task:
- <what to change>
Repo pointers:
- <file paths + approximate line numbers>
Constraints:
- OUTPUT: Unified Diff Patch ONLY.
- Minimal, focused changes. No unrelated refactors.
Output:
- A single unified diff patch.Review (audit an existing diff)
Task:
- Review the following unified diff for correctness, edge cases, and missing tests.
Constraints:
- Return a checklist of issues + suggested fixes (no code unless requested).
Input diff:
<paste unified diff here>Implementation (worktree + acceptEdits)
Task:
- <what to implement>
Repo pointers:
- <entry file paths + approximate line numbers>
Done criteria:
- <specific acceptance criteria>
Constraints:
- Stay focused on the stated task. No unrelated refactors.
- Run the narrowest test that proves correctness.
Output:
- List of files changed and a brief summary.Run implementation with write access only in an isolated worktree (--permission-mode acceptEdits); see ../references/handoff-patterns.md.
When to upgrade to XML blocks
Use the XML blocks in ../references/prompt-blocks.md when:
- The task is multi-step and you need a
<completeness_contract>. - Correctness matters and you want a
<verification_loop>. - You're doing review/research and need
<grounding_rules>. - Claude keeps stopping early — add
<default_follow_through_policy>.
See ../references/prompt-recipes.md for ready-to-use end-to-end templates.
Claude Code CLI Reference
Verified against claude (Claude Code) CLI v2.1.176. The bridge wraps claude --print (headless mode).
Invocation shape
Claude runs headlessly from the workspace directory (it uses the process cwd; there is no --cd flag — the bridge sets cwd for you):
cd /path/to/repo && claude --print "<prompt>" --output-format stream-json --verboseThe prompt is the positional argument, or piped via stdin when the bridge's --prompt-file is used.
Core flags (headless)
| Flag | Purpose |
|---|---|
-p, --print | Non-interactive mode (required for everything below) |
--output-format | text · json (single result object) · stream-json (NDJSON events; needs --verbose) |
--input-format | text (default) · stream-json |
--include-partial-messages | Token-level deltas (stream-json only) |
--model | Alias (haiku, sonnet, opus, fable) or full id (claude-opus-4-8) |
--fallback-model | Comma-separated fallbacks when the primary is overloaded |
--effort | low · medium · high · xhigh · max (model-dependent) |
--max-budget-usd | Hard USD cap; stops with subtype: error_max_budget_usd |
--max-turns | Cap agentic turns; stops with subtype: error_max_turns |
--json-schema | Structured-output schema (see caveat below) |
Permission & tools (single source of truth)
--permission-mode | Behavior |
|---|---|
plan | Read/analyze only; no edits, commands, or network tools (WebFetch/WebSearch are denied too — verified). |
dontAsk | Denies anything not in permissions.allow rules / --allowedTools. |
default | Headless (-p) cannot prompt: each gated action is denied and recorded in permission_denials. |
acceptEdits | Auto-approves file edits. |
auto | Classifier-gated auto-approval; aborts under -p if it keeps blocking. |
bypassPermissions | Bypass all checks. Sandboxed/trusted dirs only. (--dangerously-skip-permissions is the equivalent raw CLI flag.) |
Headless gating extends to reads: paths outside --cd / --add-dir are permission-gated and denied under -p (verified). Denials also surface as tool_result errors, so the bridge's tools_failed counts them.
Tool scoping:
--tools "Read,Glob,Grep"— restrict which built-ins exist at all (""= none,"default"= all).--allowedTools/--disallowedTools— approve/deny by name or rule. Footgun: the space inBash(git diff *)is load-bearing —Bash(git diff*)would also matchgit diff-index.
Safe read-only review: --permission-mode plan, or --tools "Read,Glob,Grep" --permission-mode dontAsk.
Network is tool policy, not an OS sandbox: an allowed Bash reaches the network freely, and plan blocks WebFetch/WebSearch. For read-only work that needs targeted web access (verified recipe):
--permission-mode dontAsk --tools "Read,Glob,Grep,WebFetch,WebSearch" \
--allowedTools "WebFetch(domain:example.com)" --allowedTools "WebSearch"Reproducibility & context
| Flag | Purpose |
|---|---|
--bare | Skip hooks/skills/plugins/MCP/CLAUDE.md/keychain. Reproducible; auth must be `ANTHROPIC_API_KEY` or apiKeyHelper (OAuth/keychain are not read). Slated to become the -p default. |
--safe-mode | Disable customizations but keep normal auth/model/permissions (troubleshooting). |
--add-dir | Grant access to extra directories (e.g. CLAUDE.md dirs under --bare). |
--system-prompt[-file] | Replace the system prompt (string or file). |
--append-system-prompt[-file] | Append to the system prompt (string or file). |
--mcp-config / --strict-mcp-config | Load / restrict MCP servers. |
--settings / --setting-sources | Load settings; choose sources (user/project/local). |
--agents / --agent | Define / select custom subagents. |
Sessions
| Flag | Semantics |
|---|---|
--resume <id> | Resume a specific session (bridge --SESSION_ID). Requires the same cwd. |
--continue | Resume the most recent session in the cwd. |
--session-id <uuid> | Assign a pre-chosen session UUID. |
--fork-session | With resume/continue, branch to a new session id. |
--no-session-persistence | Don't write the transcript (cannot resume later). |
Transcripts live under ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl; resume needs a matching cwd.
There is no local review / apply / fork subcommand (unlike Codex). Use --fork-session to branch, pass diffs in the prompt for review, and git apply to apply patches. claude ultrareview exists but is a cloud-hosted multi-agent review of the current branch/PR, not a local headless run. Native claude --worktree <name> creates a session worktree (verify it composes with --print before scripting).
stream-json event schema
With --output-format stream-json --verbose, each stdout line is one JSON event:
// session metadata — carries session_id, model, tools, mcp_servers
{"type":"system","subtype":"init","session_id":"…","model":"claude-sonnet-4-6"}
// assistant turn — text + tool_use blocks in message.content[]
{"type":"assistant","message":{"content":[{"type":"text","text":"…"},
{"type":"tool_use","name":"Read"}]},"session_id":"…"}
// tool results fed back — is_error:true marks failures and permission denials
{"type":"user","message":{"content":[{"type":"tool_result","is_error":true,"content":"…"}]}}
// rate-limit signal
{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"}}
// final, authoritative termination event
{"type":"result","subtype":"success","is_error":false,"result":"…final text…",
"session_id":"…","total_cost_usd":0.04,"num_turns":2,
"usage":{"input_tokens":3,"output_tokens":293,"cache_read_input_tokens":40868},
"modelUsage":{"claude-sonnet-4-6":{"costUSD":0.04}},"permission_denials":[]}result.subtype ∈ success | error_max_turns | error_max_budget_usd | error_during_execution | error_max_structured_output_retries. The final text (result.result) is present only on success; a model/auth failure can emit subtype:"success" with `is_error:true` — always check is_error. --output-format json emits just this final result object.
From these events the bridge derives tools_used, per-tool tool_counts, and tools_failed (count of tool_result blocks with is_error:true — includes permission denials and ordinary tool errors).
Other system sub-events: hook_started / hook_response (non---bare only), api_retry, compact_boundary.
Caveats
- `--json-schema` validates output after generation (not constrained decoding) — malformed output is possible; validate independently.
- stdin (used by
--prompt-file) is capped at ~10 MB. - Background Bash tasks Claude spawns under
-pare terminated ~5 s after the final result. claude --helpdoes not list every flag; absence from--helpdoes not mean unavailable (--max-turns,--system-prompt-fileare real and verified).
Handoff Patterns
How much authority to give Claude, and how to bring its work back.
Read-only analysis
For diagnosis, architecture opinions, reviews, and research.
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "/path/to/repo" \
--permission-mode plan \
--PROMPT "$PROMPT"Ask for evidence, file paths, line numbers, and a compact recommendation. Verify the cited files before acting.
plan also denies WebFetch/WebSearch. If the analysis needs the web, use the scoped recipe in cli-reference.md (dontAsk + --tools + pre-approved --allowed-tools) instead of widening the permission mode. Check tools_failed / permission_denials in the result: a denied tool means Claude answered without the evidence it asked for.
File-backed handoff
When the prompt is large, generated, or shell-sensitive (backticks, $VARS), pass it via a file instead of argv:
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "/path/to/repo" \
--permission-mode plan \
--prompt-file /tmp/claude-handoff.md--prompt-file pipes the file to Claude's stdin, bypassing argv and shell-quoting limits (keep it under ~10 MB). For large system context, use --system-prompt-file / --append-system-prompt-file.
Read-only patch proposal
When you want implementation help without letting Claude edit:
- Ask for
OUTPUT: Unified Diff Patch ONLY. - Include expected behavior and the tests to satisfy.
- Run in
--permission-mode planso no writes happen.
Inspect the patch, then apply it yourself with git apply.
Isolated write handoff
Give write access only in an isolated worktree, preferably under /tmp. This is the canonical worktree pattern — other references point here.
git worktree add -b claude/<task-name> /tmp/claude-<task-name> HEAD
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "/tmp/claude-<task-name>" \
--permission-mode acceptEdits \
--PROMPT "$PROMPT"(acceptEdits auto-approves file edits; auto adds classifier-gated command approval — see cli-reference.md.) After completion:
git -C /tmp/claude-<task-name> diff
git -C /tmp/claude-<task-name> status --shortReview and port the changes deliberately. Do not merge blind. Claude also offers native claude --worktree for interactive sessions — see parallel.md.
Parallel read-only runs
Split independent questions into separate sessions: one prompt per concern, separate SESSION_IDs, plan mode, explicit output contracts. Synthesize by comparing contradictions, shared evidence, and gaps. Details in parallel.md.
State capsule
Keep the [Claude Capsule] from SKILL.md updated after each turn, so compaction or handoff does not lose the thread.
Parallel Execution and Worktree Isolation
Use when a task splits into independent runs, or when write access must be isolated.
Parallel read-only analysis
For independent analyses that don't modify files, run multiple bridge calls concurrently. In Claude Code, set run_in_background: true on each Bash tool call and monitor each via the task-output view (the bridge streams timestamped progress to stderr).
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "/project" --permission-mode plan \
--PROMPT "Analyze the auth module for correctness risks."
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "/project" --permission-mode plan \
--PROMPT "Analyze the payment module for correctness risks."After all complete, synthesize: contradictions, shared dependencies, gaps no run covered, and the concrete next action.
Claude-managed subagents
Claude can spawn its own subagents for independent lanes via --agents (inline JSON) or --agent <name>:
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "/project" --permission-mode plan \
--PROMPT "Use parallel subagents: one for security risks, one for test gaps, one for maintainability. Wait for all, then summarize only final findings with file references."Keep subagent work read-only unless each write lane has its own worktree. Ask for distilled summaries, not raw logs.
Worktree isolation
When multiple write-capable runs are needed, use one git worktree per task (canonical setup in handoff-patterns.md):
git worktree add -b claude/auth-fix /tmp/wt-auth HEAD
git worktree add -b claude/perf-fix /tmp/wt-perf HEAD
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "/tmp/wt-auth" --permission-mode acceptEdits \
--PROMPT "Fix the auth bug in src/auth/login.py. Run the narrow verification."Review each (git -C /tmp/wt-auth diff) before merging, then git worktree remove /tmp/wt-auth.
Claude also offers native claude --worktree <name> to create a session worktree directly (verify it composes with --print for your version before relying on it in scripts).
Worktree tips
- Put worktrees in
/tmpor outside the repo. node_modules, virtualenvs, caches, and build outputs are not shared automatically.- The same branch cannot be checked out in multiple worktrees.
- Use absolute paths for result files and review artifacts.
Rate limits
- Start with 2–3 concurrent runs.
- Stagger launches if rate-limit events appear (the bridge surfaces
rate_limitedin its result). - Split by independent concern; don't run duplicate agents on the same vague task.
Prompt Patterns
When to delegate to Claude, and how to shape the prompt.
When to delegate
| Good fit | Poor fit |
|---|---|
| Large-scale code search or call-chain tracing | Simple edits faster done directly |
| Bug investigation in unfamiliar code | Tasks needing real-time user interaction |
| Cross-model / second-opinion code review | Work involving secrets or production data |
| Architecture comparison grounded in repo files | Questions already answered by current context |
| Parallel analysis of independent concerns | Vague prompts with no scope or expected output |
Pattern overview
| Pattern | Scenario | Permission mode |
|---|---|---|
| Deep analysis | Root cause, architecture, data flow | plan |
| Code review | Pre-commit, PR, security pass | plan |
| Parallel research | Multiple independent questions | plan |
| Prototyping | Draft code or scaffold | plan diff, or acceptEdits/auto in a worktree |
| Architecture comparison | Evaluate design alternatives | plan |
Prefer --permission-mode plan (analyze, no writes) or --tools "Read,Glob,Grep" for read-only work. Full permission-mode set in cli-reference.md.
Deep analysis
In this codebase, we are seeing:
<symptom, error, or failing command>
Known clues:
- <file or module>
- <recent change>
- <related stack trace>
Analyze:
1. root cause with file:line evidence
2. full code path involved
3. smallest safe fix approach
Do not modify files.Bug fix proposal
Bug: <what is happening>
Reproduction: <steps or failing test>
Expected: <correct behavior>
Investigate root cause. Check:
- <paths>
- <related components>
Output: root cause analysis with file:line evidence, then a unified diff fix.
Do not modify files directly.After review, apply with git apply (Claude has no separate apply subcommand).
Code review
Pass the diff in the prompt (Claude has no built-in review subcommand):
Review this diff for correctness, edge cases, and missing tests:
<unified diff>
Output prioritized findings with severity and suggested fixes. Skip formatting-only changes.Or point Claude at the changed paths in plan mode and let it read them itself.
Prototyping
Implement <feature> in <project>.
Requirements:
- <requirement>
Reference:
- <existing similar code path>
Constraints:
- follow existing patterns
- no unrelated refactors
Output: unified diff patch only.
Do not modify files directly.For direct writes, use an isolated worktree with --permission-mode acceptEdits (or auto). See handoff-patterns.md.
Architecture comparison
We need to implement <feature>. Compare:
Option A: <description>
Option B: <description>
Based on actual code under <paths>, evaluate:
1. implementation complexity
2. performance implications
3. impact on existing code
4. maintainability
Output a comparison table with a recommendation.Multi-file refactoring
Refactor <what> across <scope>.
Rules:
- <rule>
Files:
- <glob or directory>
Analyze impact first, list affected files, then produce a unified diff.
Do not modify files directly.Tips
- Point, don't paste: pin file paths and line numbers; let Claude read via
--cd/--add-dir. - One objective per run; split unrelated work into separate runs.
- State done criteria and output shape (table, JSON, unified diff).
- Verify Claude's output before changing final code or reporting to the user — the bridge plumbing is reliable, but the model's reasoning still needs checking.
- Tune effort/cost with
--effort(low→max),--model(sonnet/opus), and--max-budget-usd. See cli-reference.md.
Prompt Anti-Patterns
Common mistakes when prompting Claude. Each shows the problem and a fix.
Vague task framing
Bad:
Take a look at this and let me know what you think.Fix — state the job:
<task>
Review this change for material correctness and regression risks.
</task>Missing output contract
Bad:
Investigate and report back.Fix — define the shape:
<structured_output_contract>
Return:
1. root cause
2. evidence
3. smallest safe next step
</structured_output_contract>No follow-through default
Bad:
Debug this failure.Fix — tell Claude when to stop:
<default_follow_through_policy>
Keep going until you have enough evidence to identify the root cause confidently.
</default_follow_through_policy>Asking for more reasoning instead of a better contract
Bad:
Think harder and be very smart.Fix — add a verification loop:
<verification_loop>
Before finalizing, verify that the answer matches the observed evidence and task requirements.
</verification_loop>Mixing unrelated jobs into one run
Bad:
Review this diff, fix the bug you find, update the docs, and suggest a roadmap.Fix — one task per run: 1. Run review first. 2. Run a separate fix prompt if needed. 3. Use a third run for docs or roadmap.
Unsupported certainty
Bad:
Tell me exactly why production failed.Fix — require grounding:
<grounding_rules>
Ground every claim in the provided context or tool outputs.
If a point is an inference, label it clearly.
</grounding_rules>Prompt Blocks
Composable XML-tagged blocks for structuring Claude prompts. Use selectively — pick only the blocks your task needs.
Core
<task>
Use in every prompt. State the job, the context, and what done looks like.
<task>
Describe the concrete job, the relevant repository or failure context, and the expected end state.
</task>Output shape
<structured_output_contract>
Use when the response shape matters (reviews, diagnostics, recommendations).
<structured_output_contract>
Return exactly the requested output shape and nothing else.
Keep the answer compact.
Put the highest-value findings or decisions first.
</structured_output_contract><compact_output_contract>
Use when you want concise prose instead of a schema.
<compact_output_contract>
Keep the final answer compact and structured.
Do not include long scene-setting or repeated recap.
</compact_output_contract>Follow-through
<default_follow_through_policy>
Use when Claude should act without asking routine questions.
<default_follow_through_policy>
Default to the most reasonable low-risk interpretation and keep going.
Only stop to ask questions when a missing detail changes correctness, safety, or an irreversible action.
</default_follow_through_policy><completeness_contract>
Use for debugging, implementation, or any multi-step task that should not stop early.
<completeness_contract>
Resolve the task fully before stopping.
Do not stop at the first plausible answer.
Check whether there are follow-on fixes, edge cases, or cleanup needed for a correct result.
</completeness_contract><verification_loop>
Use when correctness matters.
<verification_loop>
Before finalizing, verify the result against the task requirements and the changed files or tool outputs.
If a check fails, revise the answer instead of reporting the first draft.
</verification_loop>Grounding
<missing_context_gating>
Use when Claude might otherwise guess about repository facts.
<missing_context_gating>
Do not guess missing repository facts.
If required context is absent, retrieve it with tools or state exactly what remains unknown.
</missing_context_gating><grounding_rules>
Use for review, research, or root-cause analysis.
<grounding_rules>
Ground every claim in the provided context or your tool outputs.
Do not present inferences as facts.
If a point is a hypothesis, label it clearly.
</grounding_rules><citation_rules>
Use when external research or source references matter.
<citation_rules>
Back important claims with citations or explicit references to the source material you inspected.
Prefer primary sources.
</citation_rules>Safety and scope
<action_safety>
Use for write-capable or potentially broad tasks.
<action_safety>
Keep changes tightly scoped to the stated task.
Avoid unrelated refactors, renames, or cleanup unless they are required for correctness.
Call out any risky or irreversible action before taking it.
</action_safety><tool_persistence_rules>
Use for long-running tool-heavy tasks.
<tool_persistence_rules>
Keep using tools until you have enough evidence to finish the task confidently.
Do not abandon the workflow after a partial read when another targeted check would change the answer.
</tool_persistence_rules>Task-specific
<research_mode>
Use for exploration, comparisons, or recommendations.
<research_mode>
Separate observed facts, reasoned inferences, and open questions.
Prefer breadth first, then go deeper only where the evidence changes the recommendation.
</research_mode><dig_deeper_nudge>
Use for review and adversarial inspection.
<dig_deeper_nudge>
After you find the first plausible issue, check for second-order failures, empty-state behavior, retries, stale state, and rollback paths before you finalize.
</dig_deeper_nudge>Prompt Recipes
End-to-end prompt templates for common Claude tasks. Copy the smallest recipe that fits, then trim what you don't need.
Blocks reference: prompt-blocks.md
Diagnosis
Find the root cause of a failing test, command, or runtime error.
<task>
Diagnose why [failing test / command / error] is breaking in this repository.
Use the available repository context and tools to identify the most likely root cause.
</task>
<compact_output_contract>
Return a compact diagnosis with:
1. most likely root cause
2. evidence
3. smallest safe next step
</compact_output_contract>
<default_follow_through_policy>
Keep going until you have enough evidence to identify the root cause confidently.
Only stop to ask questions when a missing detail changes correctness materially.
</default_follow_through_policy>
<verification_loop>
Before finalizing, verify that the proposed root cause matches the observed evidence.
</verification_loop>
<missing_context_gating>
Do not guess missing repository facts.
If required context is absent, state exactly what remains unknown.
</missing_context_gating>Narrow fix
Implement the smallest safe fix for an identified issue.
<task>
Implement the smallest safe fix for [issue] in this repository.
Preserve existing behavior outside the failing path.
</task>
<structured_output_contract>
Return:
1. summary of the fix
2. touched files
3. verification performed
4. residual risks or follow-ups
</structured_output_contract>
<default_follow_through_policy>
Default to the most reasonable low-risk interpretation and keep going.
</default_follow_through_policy>
<completeness_contract>
Resolve the task fully before stopping.
Do not stop after identifying the issue without applying the fix.
</completeness_contract>
<verification_loop>
Before finalizing, verify that the fix matches the task requirements and that the changed code is coherent.
</verification_loop>
<action_safety>
Keep changes tightly scoped to the stated task.
Avoid unrelated refactors or cleanup.
</action_safety>Root-cause review
Analyze a change for correctness and regression risks.
<task>
Analyze this change for material correctness and regression issues.
Focus on the provided repository context only.
</task>
<structured_output_contract>
Return:
1. findings ordered by severity
2. supporting evidence for each finding
3. brief next steps
</structured_output_contract>
<grounding_rules>
Ground every claim in the repository context or tool outputs.
If a point is an inference, label it clearly.
</grounding_rules>
<dig_deeper_nudge>
Check for second-order failures, empty-state handling, retries, stale state, and rollback paths before finalizing.
</dig_deeper_nudge>
<verification_loop>
Before finalizing, verify that each finding is material and actionable.
</verification_loop>Research / recommendation
Explore options and recommend a path.
<task>
Research the available options and recommend the best path for [topic].
</task>
<structured_output_contract>
Return:
1. observed facts
2. reasoned recommendation
3. tradeoffs
4. open questions
</structured_output_contract>
<research_mode>
Separate observed facts, reasoned inferences, and open questions.
Prefer breadth first, then go deeper only where the evidence changes the recommendation.
</research_mode>
<citation_rules>
Back important claims with explicit references to the sources you inspected.
Prefer primary sources.
</citation_rules>Shell quoting for --PROMPT
When invoking claude_bridge.py, be careful: your shell parses the command line before Python runs.
The pitfall: Markdown backticks
Markdown inline code uses backticks (` like/this `). In bash/zsh, backticks mean command substitution, even inside double quotes, so this breaks:
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "." \
--PROMPT "Analyze `tmp/eth_dev_news_raw.json` and summarize."Typical symptoms (from the shell, before Claude runs):
zsh: permission denied: tmp/eth_dev_news_raw.jsonzsh: command not found: as_of
Recommended: heredoc (no expansion)
Build the prompt via a single-quoted heredoc delimiter (<<'EOF') so backticks (and $VARS, $(...), etc.) are not expanded by the shell:
PROMPT="$(cat <<'EOF'
Analyze `tmp/eth_dev_news_raw.json` and summarize.
Set `as_of` to `YYYY-MM-DD`.
EOF
)"
python3 skills/collaborating-with-claude/scripts/claude_bridge.py \
--cd "." \
--PROMPT "$PROMPT" \
--output-format stream-jsonAlternatives
- Escape backticks manually: use
\` (easy to miss in long prompts). - Avoid backticks entirely: write
Analyze the file tmp/eth_dev_news_raw.jsoninstead.
#!/usr/bin/env python3
"""
Claude Code Bridge Script.
Wraps the Claude Code CLI (`claude --print`) to provide a JSON interface,
live stderr progress, multi-turn sessions via SESSION_ID, and structured
result telemetry (termination reason, cost, tokens, turns).
Verified against `claude` (Claude Code) CLI v2.1.176.
"""
from __future__ import annotations
import argparse
import json
import os
import queue
import shutil
import subprocess
import sys
import threading
import time
from pathlib import Path
from typing import Any, Dict, Generator, List, Optional
def emit_json(result: Dict[str, Any], exit_code: int = 0) -> None:
print(json.dumps(result, indent=2, ensure_ascii=False))
raise SystemExit(exit_code)
def find_executable(name: str) -> str:
"""Locate an executable while handling Windows npm shims."""
found = shutil.which(name)
if found:
if os.name == "nt" and not Path(found).suffix:
for ext in (".cmd", ".bat", ".exe"):
alt = Path(found).parent / f"{name}{ext}"
if alt.is_file():
return str(alt)
return found
if os.name == "nt":
for env_var in ("APPDATA", "LOCALAPPDATA"):
base = os.environ.get(env_var, "")
if base:
for ext in (".cmd", ".bat", ".exe"):
candidate = Path(base) / "npm" / f"{name}{ext}"
if candidate.is_file():
return str(candidate)
return name
def preflight_check(cd: Path) -> Optional[str]:
if shutil.which("claude") is None:
return "Claude Code CLI not found in PATH. Install it and ensure `claude` is available."
if not cd.exists():
return f"Workspace root `{cd.absolute().as_posix()}` does not exist."
if not cd.is_dir():
return f"Workspace root `{cd.absolute().as_posix()}` is not a directory."
return None
def extract_assistant_text(message: Any) -> str:
"""Pull concatenated text blocks from an assistant message object."""
if not isinstance(message, dict):
return ""
content = message.get("content")
if isinstance(content, str):
return content
parts: List[str] = []
if isinstance(content, list):
for block in content:
if (
isinstance(block, dict)
and block.get("type") == "text"
and isinstance(block.get("text"), str)
):
parts.append(block["text"])
return "".join(parts)
def stream_command(
cmd: List[str],
cwd: Optional[Path] = None,
timeout_seconds: float = 0,
stdin_file: Optional[Path] = None,
) -> Generator[str, None, int]:
"""Execute a command and yield stdout lines while forwarding stderr progress."""
resolved = cmd.copy()
resolved[0] = find_executable(cmd[0])
if os.name == "nt" and Path(resolved[0]).suffix.lower() in {".cmd", ".bat"}:
comspec = os.environ.get("COMSPEC", "cmd.exe")
resolved = [comspec, "/d", "/s", "/c", " ".join(f'"{arg}"' for arg in resolved)]
stdin_handle = None
try:
stdin_target: Any = subprocess.DEVNULL
if stdin_file is not None:
stdin_handle = stdin_file.open("r", encoding="utf-8", errors="replace")
stdin_target = stdin_handle
proc = subprocess.Popen(
resolved,
shell=False,
stdin=stdin_target,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
encoding="utf-8",
errors="replace",
cwd=str(cwd) if cwd is not None else None,
)
finally:
if stdin_handle is not None:
stdin_handle.close()
out_q: "queue.Queue[Optional[str]]" = queue.Queue()
started_at = time.time()
def read_stdout() -> None:
assert proc.stdout is not None
for line in iter(proc.stdout.readline, ""):
stripped = line.strip()
if stripped:
out_q.put(stripped)
proc.stdout.close()
out_q.put(None)
def read_stderr() -> None:
assert proc.stderr is not None
for line in iter(proc.stderr.readline, ""):
text = line.rstrip()
if text:
print(f"[claude stderr] {text}", file=sys.stderr, flush=True)
proc.stderr.close()
t_out = threading.Thread(target=read_stdout, daemon=True)
t_err = threading.Thread(target=read_stderr, daemon=True)
t_out.start()
t_err.start()
while True:
if timeout_seconds and time.time() - started_at > timeout_seconds:
print(
f"[claude] timeout after {timeout_seconds:g}s; terminating Claude Code",
file=sys.stderr,
flush=True,
)
proc.kill()
return proc.wait()
try:
line = out_q.get(timeout=0.5)
if line is None:
break
yield line
except queue.Empty:
if proc.poll() is not None and not t_out.is_alive():
break
t_out.join(timeout=5)
t_err.join(timeout=2)
try:
return proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
return proc.wait()
def apply_result_event(event: Dict[str, Any], state: Dict[str, Any]) -> None:
"""Copy fields from a `type:result` event into bridge state."""
state["subtype"] = event.get("subtype")
state["is_error"] = bool(event.get("is_error"))
if isinstance(event.get("result"), str):
state["result_text"] = event["result"]
state["total_cost_usd"] = event.get("total_cost_usd")
state["usage"] = event.get("usage") or {}
state["num_turns"] = event.get("num_turns")
state["permission_denials"] = event.get("permission_denials") or []
state["terminal_reason"] = event.get("terminal_reason")
state["stop_reason"] = event.get("stop_reason")
state["duration_ms"] = event.get("duration_ms")
model_usage = event.get("modelUsage")
if isinstance(model_usage, dict) and model_usage and not state.get("model"):
state["model"] = next(iter(model_usage))
if event.get("structured_output") is not None:
state["structured_output"] = event.get("structured_output")
def summarize_event(
event: Dict[str, Any],
all_messages: List[Dict[str, Any]],
state: Dict[str, Any],
start_time: float,
) -> None:
"""Update state from a stream-json event and emit a stderr progress line."""
all_messages.append(event)
def status(message: str) -> None:
elapsed = time.time() - start_time
print(f"[claude {elapsed:5.1f}s] {message}", file=sys.stderr, flush=True)
if event.get("session_id") and not state["session_id"]:
state["session_id"] = event["session_id"]
etype = event.get("type", "")
if etype == "system":
subtype = event.get("subtype", "")
if subtype == "init":
state["session_id"] = event.get("session_id") or state["session_id"]
state["model"] = event.get("model")
status(f"Session {state['session_id']} · model {event.get('model', '?')}")
elif subtype == "api_retry":
status(f"API retry {event.get('attempt')}/{event.get('max_retries')} ({event.get('error', '?')})")
elif subtype == "compact_boundary":
status("Context compacted")
elif etype == "assistant":
message = event.get("message", {})
text = extract_assistant_text(message)
if text:
state["agent_messages"] += text
preview = text[:80].replace("\n", " ")
status(f"Response: {preview}{'…' if len(text) > 80 else ''}")
content = message.get("content")
if isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "tool_use":
state["tools_used"] += 1
name = str(block.get("name", "?"))
state["tool_counts"][name] = state["tool_counts"].get(name, 0) + 1
status(f"Tool: {name}")
elif etype == "user":
content = event.get("message", {}).get("content")
if isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "tool_result" and block.get("is_error"):
state["tools_failed"] += 1
body = block.get("content")
text = body if isinstance(body, str) else ""
if isinstance(body, list):
text = " ".join(
part.get("text", "") for part in body if isinstance(part, dict)
)
preview = text.strip().replace("\n", " ")[:80]
status(f"Tool error: {preview}" if preview else "Tool error")
elif etype == "rate_limit_event":
info = event.get("rate_limit_info", {})
if isinstance(info, dict) and info.get("status") and info.get("status") != "allowed":
state["rate_limited"] = True
status(f"Rate limit: {info.get('status')} ({info.get('rateLimitType', '?')})")
elif etype == "result":
apply_result_event(event, state)
cost = state["total_cost_usd"]
cost_s = f"${cost:.4f}" if isinstance(cost, (int, float)) else "?"
usage = state["usage"] or {}
status(
f"Done · {state['subtype']} · {cost_s} · "
f"{usage.get('input_tokens', 0) or 0} in / {usage.get('output_tokens', 0) or 0} out · "
f"{state['num_turns'] or 0} turns"
)
if "error" in etype and etype != "result":
message = ""
error_obj = event.get("error")
if isinstance(error_obj, dict):
message = str(error_obj.get("message", ""))
message = message or str(event.get("message", ""))
if message:
state["errors"].append(message)
def parse_json_blob(raw_lines: List[str], all_messages: List[Dict[str, Any]], state: Dict[str, Any]) -> None:
"""Parse non-streaming `--output-format json` output (a single result object)."""
raw = "\n".join(raw_lines).strip()
if not raw:
state["errors"].append("No output received from Claude Code.")
return
try:
parsed = json.loads(raw)
except json.JSONDecodeError as error:
state["errors"].append(f"Failed to parse JSON output: {error}")
return
objects = parsed if isinstance(parsed, list) else [parsed]
for obj in objects:
if not isinstance(obj, dict):
continue
all_messages.append(obj)
if obj.get("session_id") and not state["session_id"]:
state["session_id"] = obj["session_id"]
if obj.get("type") == "result" or "subtype" in obj:
apply_result_event(obj, state)
elif obj.get("type") == "assistant":
state["agent_messages"] += extract_assistant_text(obj.get("message", {}))
def build_command(args: argparse.Namespace, prompt_arg: Optional[str]) -> List[str]:
cmd = ["claude", "--print"]
if prompt_arg is not None:
cmd.append(prompt_arg)
cmd.extend(["--output-format", args.output_format, "--input-format", args.input_format])
if args.include_partial_messages:
cmd.append("--include-partial-messages")
if args.output_format == "stream-json" or args.verbose:
cmd.append("--verbose")
if args.model:
cmd.extend(["--model", args.model])
if args.effort:
cmd.extend(["--effort", args.effort])
if args.bare:
cmd.append("--bare")
if args.safe_mode:
cmd.append("--safe-mode")
if args.fallback_model:
cmd.extend(["--fallback-model", args.fallback_model])
if args.max_budget_usd:
cmd.extend(["--max-budget-usd", args.max_budget_usd])
if args.max_turns:
cmd.extend(["--max-turns", args.max_turns])
if args.json_schema:
cmd.extend(["--json-schema", args.json_schema])
if args.continue_session:
cmd.append("--continue")
if args.SESSION_ID:
cmd.extend(["--resume", args.SESSION_ID])
if args.session_id:
cmd.extend(["--session-id", args.session_id])
if args.fork_session:
cmd.append("--fork-session")
if args.no_session_persistence:
cmd.append("--no-session-persistence")
for extra_dir in args.add_dir:
cmd.extend(["--add-dir", extra_dir])
if args.system_prompt:
cmd.extend(["--system-prompt", args.system_prompt])
if args.system_prompt_file:
cmd.extend(["--system-prompt-file", args.system_prompt_file])
if args.append_system_prompt:
cmd.extend(["--append-system-prompt", args.append_system_prompt])
if args.append_system_prompt_file:
cmd.extend(["--append-system-prompt-file", args.append_system_prompt_file])
for tool in args.allowed_tools:
cmd.extend(["--allowedTools", tool])
if args.tools:
cmd.extend(["--tools", args.tools])
for tool in args.disallowed_tools:
cmd.extend(["--disallowedTools", tool])
if args.permission_mode:
cmd.extend(["--permission-mode", args.permission_mode])
if args.permission_prompt_tool:
cmd.extend(["--permission-prompt-tool", args.permission_prompt_tool])
for cfg in args.mcp_config:
cmd.extend(["--mcp-config", cfg])
if args.strict_mcp_config:
cmd.append("--strict-mcp-config")
for settings in args.settings:
cmd.extend(["--settings", settings])
if args.setting_sources:
cmd.extend(["--setting-sources", args.setting_sources])
if args.agent:
cmd.extend(["--agent", args.agent])
if args.agents:
cmd.extend(["--agents", args.agents])
return cmd
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Claude Code Bridge")
parser.add_argument("--PROMPT", default="", help="Instruction to send to claude. Use this or --prompt-file.")
parser.add_argument("--prompt-file", type=Path, default=None, help="Read the prompt from a file and pipe it to claude via stdin (avoids argv/shell-quoting limits).")
parser.add_argument("--cd", required=True, type=Path, help="Set the workspace root for claude before executing the task.")
session_group = parser.add_mutually_exclusive_group()
session_group.add_argument("--SESSION_ID", default="", help="Resume the specified session of claude.")
session_group.add_argument("--session-id", dest="session_id", default="", help="Use a specific session ID (UUID).")
session_group.add_argument("--continue", dest="continue_session", action="store_true", help="Continue the most recent session.")
parser.add_argument("--fork-session", action="store_true", help="Fork session when resuming/continuing.")
parser.add_argument("--no-session-persistence", action="store_true", help="Disable session persistence (print mode only).")
parser.add_argument("--model", default="", help="Model override (alias like 'sonnet'/'opus', or a full model name).")
parser.add_argument("--effort", default="", choices=["", "low", "medium", "high", "xhigh", "max"], help="Reasoning effort level (model-dependent).")
parser.add_argument("--bare", action="store_true", help="Minimal mode: skip hooks/skills/plugins/MCP/CLAUDE.md/keychain. Requires ANTHROPIC_API_KEY or apiKeyHelper.")
parser.add_argument("--safe-mode", dest="safe_mode", action="store_true", help="Disable customizations (CLAUDE.md/skills/plugins/hooks/MCP) but keep normal auth/model/permissions.")
parser.add_argument("--fallback-model", default="", help="Fallback model(s), comma-separated, when the default is overloaded.")
parser.add_argument("--max-budget-usd", default="", help="Max USD budget for the call (print mode only).")
parser.add_argument("--max-turns", default="", help="Maximum agentic turns before stopping (print mode only).")
parser.add_argument("--json-schema", default="", help="JSON schema for structured output (validated post-generation, not constrained decoding).")
parser.add_argument("--input-format", default="text", choices=["text", "stream-json"], help="Claude input format.")
parser.add_argument("--add-dir", action="append", default=[], help="Add additional working directories.")
parser.add_argument("--append-system-prompt", default="", help="Append text to the default system prompt.")
parser.add_argument("--append-system-prompt-file", default="", help="Append a file's contents to the default system prompt.")
parser.add_argument("--system-prompt", default="", help="Replace the system prompt for the session.")
parser.add_argument("--system-prompt-file", default="", help="Replace the system prompt with a file's contents.")
parser.add_argument("--allowed-tools", action="append", default=[], help="Tools to allow without prompting.")
parser.add_argument("--disallowed-tools", action="append", default=[], help="Tools to remove from context.")
parser.add_argument("--tools", default="", help='Built-in tools to enable: "" (none), "default", or e.g. "Read,Glob,Grep".')
parser.add_argument("--permission-mode", default="", help="Permission mode: default/plan/acceptEdits/auto/dontAsk/bypassPermissions.")
parser.add_argument("--permission-prompt-tool", default="", help="MCP tool to handle permission prompts.")
parser.add_argument("--mcp-config", action="append", default=[], help="Load MCP servers from JSON files or strings.")
parser.add_argument("--strict-mcp-config", action="store_true", help="Only use MCP servers from --mcp-config.")
parser.add_argument("--settings", action="append", default=[], help="Load settings from JSON files or strings.")
parser.add_argument("--setting-sources", default="", help="Comma-separated list of setting sources.")
parser.add_argument("--agent", default="", help="Agent name to use for the session.")
parser.add_argument("--agents", default="", help="JSON defining custom agents.")
parser.add_argument("--output-format", default="stream-json", choices=["text", "json", "stream-json"], help="Claude output format.")
parser.add_argument("--include-partial-messages", action="store_true", help="Include partial streaming events.")
parser.add_argument("--verbose", action="store_true", help="Enable verbose CLI output (required for stream-json).")
parser.add_argument(
"--timeout",
type=float,
default=0,
help="Terminate Claude after this many seconds. Default: no bridge timeout.",
)
parser.add_argument(
"--return-all-messages",
action="store_true",
help="Return all messages (e.g. tool calls, traces) from the claude session.",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.timeout < 0:
emit_json({"success": False, "error": "`--timeout` must be zero or a positive number of seconds."}, exit_code=2)
# Resolve the prompt source: exactly one of --PROMPT / --prompt-file.
if args.PROMPT and args.prompt_file is not None:
emit_json({"success": False, "error": "Use either `--PROMPT` or `--prompt-file`, not both."}, exit_code=2)
if not args.PROMPT and args.prompt_file is None:
emit_json({"success": False, "error": "Provide `--PROMPT` or `--prompt-file`."}, exit_code=2)
stdin_file: Optional[Path] = None
prompt_arg: Optional[str] = args.PROMPT
if args.prompt_file is not None:
if not args.prompt_file.is_file():
emit_json({"success": False, "error": f"Prompt file not found: {args.prompt_file}"}, exit_code=2)
prompt_arg = None
stdin_file = args.prompt_file
cd: Path = args.cd
error = preflight_check(cd)
if error:
emit_json({"success": False, "error": error}, exit_code=1)
warnings: List[str] = []
if args.bare and not os.environ.get("ANTHROPIC_API_KEY"):
warnings.append(
"--bare ignores OAuth/keychain auth; set ANTHROPIC_API_KEY (or apiKeyHelper via --settings) or the call will fail to authenticate."
)
cmd = build_command(args, prompt_arg)
all_messages: List[Dict[str, Any]] = []
state: Dict[str, Any] = {
"agent_messages": "",
"result_text": None,
"session_id": None,
"subtype": None,
"is_error": False,
"total_cost_usd": None,
"usage": {},
"num_turns": None,
"tools_used": 0,
"tools_failed": 0,
"tool_counts": {},
"permission_denials": [],
"terminal_reason": None,
"stop_reason": None,
"duration_ms": None,
"structured_output": None,
"rate_limited": False,
"model": None,
"errors": [],
}
raw_json_lines: List[str] = []
start_time = time.time()
returncode = 1
try:
generator = stream_command(cmd, cwd=cd.absolute(), timeout_seconds=args.timeout, stdin_file=stdin_file)
while True:
try:
line = next(generator)
except StopIteration as finished:
returncode = int(finished.value or 0)
break
if args.output_format == "stream-json":
try:
event = json.loads(line)
except json.JSONDecodeError:
state["errors"].append(f"[json decode error] {line}")
continue
if isinstance(event, dict):
summarize_event(event, all_messages, state, start_time)
elif args.output_format == "json":
raw_json_lines.append(line)
else: # text
state["agent_messages"] += line + "\n"
except Exception as exc: # pragma: no cover - defensive boundary
emit_json({"success": False, "error": f"Failed to run Claude Code: {exc}", "warnings": warnings}, exit_code=1)
if args.output_format == "json":
parse_json_blob(raw_json_lines, all_messages, state)
elif args.output_format == "text":
state["agent_messages"] = state["agent_messages"].strip()
agent_messages = state["agent_messages"] or state["result_text"] or ""
# Determine success: prefer the authoritative result-event subtype; fall back to exit code.
if state["subtype"] is not None:
success = state["subtype"] == "success" and not state["is_error"]
else:
success = returncode == 0
session_expected = args.output_format in ("json", "stream-json") and not args.no_session_persistence
if success and state["session_id"] is None and session_expected:
warnings.append("Could not capture SESSION_ID; multi-turn resume will not be possible for this run.")
if not success:
if state["subtype"] and state["subtype"] != "success":
state["errors"].append(f"Claude terminated with subtype: {state['subtype']}.")
if returncode != 0:
state["errors"].append(f"Claude Code exited with non-zero status: {returncode}.")
if args.timeout and returncode != 0:
state["errors"].append(f"Claude may have timed out after {args.timeout:g} seconds.")
if not agent_messages and not state["errors"]:
state["errors"].append("No response captured from Claude Code.")
result: Dict[str, Any] = {"success": success}
if state["session_id"] is not None:
result["SESSION_ID"] = state["session_id"]
result["agent_messages"] = agent_messages
if state["model"]:
result["model"] = state["model"]
if state["subtype"]:
result["subtype"] = state["subtype"]
if state["is_error"]:
result["is_error"] = True
if state["total_cost_usd"] is not None:
result["total_cost_usd"] = state["total_cost_usd"]
if state["usage"]:
usage = state["usage"]
compact = {
key: usage.get(key)
for key in ("input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens")
if usage.get(key) is not None
}
if compact:
result["usage"] = compact
if state["num_turns"] is not None:
result["num_turns"] = state["num_turns"]
if state["tools_used"]:
result["tools_used"] = state["tools_used"]
if state["tools_failed"]:
result["tools_failed"] = state["tools_failed"]
if state["tool_counts"]:
result["tool_counts"] = state["tool_counts"]
if state["permission_denials"]:
result["permission_denials"] = state["permission_denials"]
if state["structured_output"] is not None:
result["structured_output"] = state["structured_output"]
if state["rate_limited"]:
result["rate_limited"] = True
if warnings:
result["warnings"] = warnings
if not success:
result["error"] = "\n".join(str(err) for err in state["errors"] if err) or "No response from Claude Code."
if args.return_all_messages:
result["all_messages"] = all_messages
print(json.dumps(result, indent=2, ensure_ascii=False))
raise SystemExit(0 if success else 1)
if __name__ == "__main__":
main()
Related skills
FAQ
How does it invoke Claude Code?
Through a bridge script that wraps claude --print and returns structured JSON; you should not invoke claude directly.
Does it support multi-turn sessions?
Yes, it captures a SESSION_ID from the first call and lets you resume by ID or with --continue.