Skip to content

claude-code provider cannot decode claude --print output: CLI 2.x returns an event array, parse_envelope expects an object #492

Description

@fedorovvvv

What happens

forgeplan reason fails on a correctly configured, logged-in claude-code provider:

$ forgeplan reason RFC-001
  Analyzing RFC-001 with ADI cycle (claude-code/claude-sonnet-5)...
Error: ADI reasoning failed: claude-code provider: failed to decode `claude --print` JSON envelope: invalid type: map, expected a boolean at line 1 column 1. Is `claude` logged in? Try `claude login`.
Error: LLM call failed

The remediation text points at authentication, which sends you to claude login, claude auth, and a re-check of .forgeplan/secrets.env. None of it is the problem — the session is logged in and claude --print works fine on its own.

Root cause

claude --print --output-format json on claude CLI 2.x returns a JSON array of stream events. The envelope is the final {"type": "result", ...} entry:

[
  {"type": "system", "subtype": "init", "session_id": "...", "tools": [...]},
  {"type": "assistant", "message": {...}},
  {"type": "result", "subtype": "success", "is_error": false, "result": "...", "session_id": "..."}
]

parse_envelope (crates/forgeplan-core/src/playbook/dispatch/claude_print.rs:485) decodes stdout straight into ClaudePrintResponse:

pub(crate) fn parse_envelope(stdout: &[u8]) -> Result<ClaudePrintResponse, serde_json::Error> {
    let s = String::from_utf8_lossy(stdout);
    serde_json::from_str(s.trim())
}

serde deserialises a JSON array into a struct positionally. The first field of ClaudePrintResponse is is_error: bool, so element 0 — {"type": "system", ...} — is fed to it, giving invalid type: map, expected a boolean at line 1 column 1. The column-1 position is what makes it look like malformed output rather than a shape mismatch.

Blast radius

parse_envelope is pub(crate) and has two consumers, so both paths are affected:

  • crate::llm — the claude-code provider (llm/mod.rs:247). This is the reported symptom: forgeplan reason is dead, and with it every Standard+ FPF ADI gate that runs before activation.
  • playbook::dispatchAgentDispatcher and PluginDispatcher (claude_print::invoke, line 683).

The existing tests do not catch it because every fixture uses the bare-object shape (json_response() in claude_print::tests, write_mock_claude() in llm::claude_code_tests) — the shape the CLI emitted when #382 landed.

Repro

# 1. see the actual shape
echo hi | claude --print --output-format json | jq 'type'     # => "array"

# 2. configure the provider — no API key needed, it reuses `claude login`
cat >> .forgeplan/config.yaml <<'YAML'
llm:
  provider: claude-code
  model: claude-sonnet-5
YAML

# 3. any artifact
forgeplan reason <ID>

Workaround

Switch to a provider that talks HTTP directly (claude + ANTHROPIC_API_KEY, gemini, openai). That reintroduces the separate API key and billing that #382 set out to avoid.

Environment

forgeplan 0.34.0 and 0.37.0 — identical failure on both
claude CLI 2.1.266
OS macOS 25.5.0 (arm64)

Fix

PR follows: unwrap the array to its last type: "result" event before decoding, keep the bare-object path for older CLI versions, and make "array with no result event" a named error instead of a positional mis-parse.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions