Claude Code Plugins

Community-maintained marketplace

Feedback

claude-code-automation

@coreyja/dotfiles
12
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name claude-code-automation
description 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 Code Automation

This skill covers running Claude Code programmatically - either one-shot with --print or multi-turn with the bidirectional stream-json protocol.

Quick Reference

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

Headless Mode (--print)

For one-shot execution without interactive prompts.

Essential Flags

claude --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

Critical Gotchas

  1. --verbose is required with stream-json output - without it, Claude exits silently with status 1

  2. File writes silently fail without --permission-mode bypassPermissions

  3. Session hooks don't run in --print mode - no SessionStart/SessionEnd

  4. --append-system-prompt is ephemeral - NOT re-applied on --resume

Rust Implementation

pub 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(())
}

Bidirectional Protocol

For multi-turn conversations with a persistent Claude process.

Starting a Session

claude --input-format stream-json --output-format stream-json --verbose

Message Types

1. Initialize (set system prompt once)

{
  "subtype": "initialize",
  "systemPrompt": "Custom system prompt",
  "appendSystemPrompt": "Additional context"
}

2. Send user messages

{
  "type": "user",
  "session_id": "",
  "message": {
    "role": "user",
    "content": [{"type": "text", "text": "Your question here"}]
  },
  "parent_tool_use_id": null
}

Critical Gotcha: control_response

control_response is a top-level type, NOT a subtype of system:

// 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");
}

Rust Session Pattern

pub 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(())
    }
}

Reading JSONL Transcripts

Claude session transcripts can be large. Direct file reads often fail.

Size limits:

  • 256KB maximum for direct reads
  • Individual JSONL lines can be 50k+ characters

Working solutions:

# 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])
"

When to Use Which

Use headless (--print) for:

  • One-off background agents
  • Each invocation needs different context
  • Truly independent sessions
  • Simple automation scripts

Use bidirectional for:

  • Multi-turn conversations
  • When system prompt must persist across messages
  • Building interactive applications
  • When --resume behavior is insufficient

Use interactive (normal) for:

  • Human conversations
  • Sessions requiring workspace trust dialogs
  • When hooks are essential