Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Run Claude Code programmatically in headless mode or via bidirectional stream-json protocol. Use when building agents, automating Claude, running headless Claude, implementing multi-turn conversations, or integrating Claude CLI into applications.
.claude/skills/majiayu000-claude-code-automation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -3% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 5% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 5% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 41% | 0% |
This skill covers running Claude Code programmatically - either one-shot with --print or multi-turn with the bidirectional stream-json protocol.
| Mode | Use Case | Key Flags | |------|----------|-----------| | Headless (--print) | One-shot tasks, background agents | --print --verbose --output-format stream-json | | Bidirectional | Multi-turn conversations, persistent sessions | --input-format stream-json --output-format stream-json --verbose |
--print)For one-shot execution without interactive prompts.
bashclaude --print \ --verbose \ # REQUIRED with stream-json --output-format stream-json \ --model sonnet \ --permission-mode bypassPermissions \ # Enable file writes --append-system-prompt "context here" # Inject dynamic context
--verbose is required with stream-json output - without it, Claude exits silently with status 1--permission-mode bypassPermissions--print mode - no SessionStart/SessionEnd--append-system-prompt is ephemeral - NOT re-applied on --resumerustpub fn run_headless_claude(prompt: &str, working_dir: &Path, system_prompt: &str) -> Result<()> { let mut cmd = Command::new("claude") .arg("--print") .arg("--verbose") .arg("--model").arg("sonnet") .arg("--output-format").arg("stream-json") .arg("--permission-mode").arg("bypassPermissions") .arg("--append-system-prompt").arg(system_prompt) .current_dir(working_dir) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; // Write prompt to stdin, process output... Ok(()) }
For multi-turn conversations with a persistent Claude process.
bashclaude --input-format stream-json --output-format stream-json --verbose
1. Initialize (set system prompt once)
json{ "subtype": "initialize", "systemPrompt": "Custom system prompt", "appendSystemPrompt": "Additional context" }
2. Send user messages
json{ "type": "user", "session_id": "", "message": { "role": "user", "content": [{"type": "text", "text": "Your question here"}] }, "parent_tool_use_id": null }
control_response is a top-level type, NOT a subtype of system:
rust// WRONG - never matches if event.get("type") == Some("system") && event.get("subtype") == Some("control_response") { ... } // CORRECT if event.get("type") == Some("control_response") { info!("Session initialized"); }
rustpub struct ClaudeSession { process: Child, stdin: ChildStdin, } impl ClaudeSession { pub fn new(system_prompt: &str) -> Result<Self> { let mut process = Command::new("claude") .arg("--input-format").arg("stream-json") .arg("--output-format").arg("stream-json") .arg("--verbose") .arg("--model").arg("sonnet") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; let mut stdin = process.stdin.take().unwrap(); // Send initialize message let init = serde_json::json!({ "subtype": "initialize", "appendSystemPrompt": system_prompt }); writeln!(stdin, "{}", init)?; stdin.flush()?; Ok(Self { process, stdin }) } pub fn send_message(&mut self, text: &str) -> Result<()> { let msg = serde_json::json!({ "type": "user", "session_id": "", "message": { "role": "user", "content": [{"type": "text", "text": text}] }, "parent_tool_use_id": null }); writeln!(self.stdin, "{}", msg)?; self.stdin.flush()?; Ok(()) } }
Claude session transcripts can be large. Direct file reads often fail.
Size limits:
Working solutions:
bash# Extract specific fields with jq jq -r 'select(.type == "user") | .message.content' file.jsonl | head -20 # Python for complex extraction python3 -c " import json for line in open('file.jsonl'): obj = json.loads(line) if obj.get('type') == 'user': print(obj.get('message', {}).get('content', '')[:200]) "
Use headless (--print) for:
Use bidirectional for:
--resume behavior is insufficientUse interactive (normal) for:
Other measured skills in the registry, with their headline benchmark lift.