Skip to content

fix: guard handle_sdk_message against non-map JSON#51

Merged
guess merged 3 commits into
guess:mainfrom
pshoukry:fix/non-map-sdk-message-guard
May 28, 2026
Merged

fix: guard handle_sdk_message against non-map JSON#51
guess merged 3 commits into
guess:mainfrom
pshoukry:fix/non-map-sdk-message-guard

Conversation

@pshoukry

Copy link
Copy Markdown
Contributor

Summary

The Claude CLI can emit non-map JSON values (true, [1,2,3], 42, …) on stdout alongside the normal stream-JSON output — for example, when a hook callback fails and a Zod schema validation error is logged. ClaudeCode.CLI.Control.classify/1 falls through to {:message, json} for any non-map, so without a guard the subsequent ClaudeCode.Adapter.Port.handle_sdk_message/2 clauses call json["type"] — which invokes Access.get/3 on the non-map and crashes the Port GenServer with a FunctionClauseError, aborting the in-flight session.

Fix

Add a when not is_map(json) clause to handle_sdk_message/2 that logs and drops the offending chunk so the session continues:

defp handle_sdk_message(json, state) when not is_map(json) do
  Logger.warning("Dropping non-map SDK message: #{inspect(json)}")
  state
end

Test

New integration test in port_integration_test.exs that uses the existing MockCLI helper to stream true, [1,2,3], and 42 between a system message and the final result, then asserts the session completes and returns the expected %ResultMessage{}. Full suite (1492 tests) passes.

Repro context

This was hit in a downstream SDK consumer when a hook returned an unexpected shape and the CLI printed Zod validation diagnostics on stdout. Without the guard the entire session terminates the moment that line is processed — even though the actual model output is fine and the result message arrives right after.

The CLI may emit non-map JSON values (booleans, arrays, numbers, strings)
on stdout when, for example, a hook callback fails and a Zod schema
validation error is printed alongside the normal stream-JSON output.

Control.classify/1 falls through to {:message, json} for any non-map, so
without a guard the subsequent handle_sdk_message/2 clauses call
json["type"], which invokes Access.get/3 on a non-map and crashes the
Port GenServer with a FunctionClauseError — aborting the in-flight
session.

Add a guard clause that logs and drops the offending chunk so the
session can continue. Covered by a MockCLI integration test that streams
true, [1,2,3], and 42 between a system message and the final result and
asserts the session still completes with the expected result.
@guess

guess commented May 22, 2026

Copy link
Copy Markdown
Owner

@pshoukry thanks for the PR!

Looks like dialyzer is failing. I'm thinking we could maybe do this check one level higher at process_line and not even try to classify if json is not a map. Then we won't incorrectly classify it as an sdk message to begin within.

Something like this:

{:ok, json} when is_map(json) ->
    case Control.classify(json) do ...

{:ok, non_map} ->
    Logger.warning("Dropping non-map CLI output: #{inspect(non_map)}")
    state

{:error, _} ->
    Logger.debug("Non-JSON CLI output: ...")
    state

What do you think?

Move the guard one level up so non-map CLI output (booleans, arrays,
numbers, strings) never reaches Control.classify/1 — which is spec'd to
take a map and was failing dialyzer when passed a non-map. Also avoids
misclassifying non-map values as SDK messages via the classify/1
catch-all.
@pshoukry

Copy link
Copy Markdown
Contributor Author

@guess sounds good, please check.

@pshoukry

Copy link
Copy Markdown
Contributor Author

@guess I applied the fix as you suggested. please check.

@guess

guess commented May 28, 2026

Copy link
Copy Markdown
Owner

@pshoukry looks like a test is failing

@pshoukry

Copy link
Copy Markdown
Contributor Author
Screenshot 2026-05-28 at 4 42 31 PM

The error looked to me like a network error and I wasn't sure if that passed before. I will take a deeper look.

…lake

`capture_log/2` installs a global Logger handler, so logs from
concurrently running async tests (e.g. session_test.exs sending an
invalid JSON `adapter_message` to provoke a warning) were leaking into
these `assert log == ""` checks and failing CI nondeterministically.

Refute only the specific "no matching system env" messages this code
path can emit, so unrelated log noise from neighbouring tests no longer
breaks the assertion.
@pshoukry

Copy link
Copy Markdown
Contributor Author

@guess I pushed a fix vs the flaky test that fails locally and pushed again. I think rerunning the test suite would have passed since this error seems to be intermittent.

@guess
guess merged commit 8d30c19 into guess:main May 28, 2026
2 checks passed
@pshoukry
pshoukry deleted the fix/non-map-sdk-message-guard branch May 28, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants