Skip to content

Malformed inline settings is silently discarded with sandbox; the file form raises a bare JSONDecodeError #1335

Description

@feiiiiii5

Two things go wrong with options.settings, in opposite directions, and neither matches the documented contract.

ClaudeAgentOptions.settings is documented as "Path to an additional settings JSON file to load, or an inline JSON string" (types.py:2105-2116). It goes on to describe the degradation for a missing file: "a missing file is logged and only the sandbox settings are passed". It says nothing about an inline string that fails to parse — and that is the case that loses your settings silently.

1. A malformed inline string is discarded, and the warning names a file that does not exist.

Calling the real _build_settings_value() at 37422c2:

valid JSON + sandbox                   -> {"model": "sonnet", "sandbox": {"enabled": true}}
MALFORMED + sandbox                    -> {"sandbox": {"enabled": true}}
MALFORMED, no sandbox (pass-through)   -> {"model":"sonnet",}
path to MALFORMED file + sandbox       -> RAISED JSONDecodeError: Illegal trailing comma before end of object
nonexistent path + sandbox             -> {"sandbox": {"enabled": true}}

where MALFORMED is '{"model":"sonnet",}'.

With sandbox also set, one trailing comma costs you the entire settings object — model, permissions, env, hooks — and the CLI starts with only the sandbox block. The run proceeds and looks fine. The only trace is this, at subprocess_cli.py:507-509:

Failed to parse settings as JSON, treating as file path: {"model":"sonnet",}

It reports a file path, and the "file path" is the JSON text itself. It never says the settings were dropped, and it is the only warning on a path that is about to lose data.

Note the contrast two lines down in the same output: with no sandbox, the malformed text is passed through verbatim and the CLI rejects it. So the more dangerous configuration is the quieter one.

2. The same bytes as a file raise a bare json.JSONDecodeError, outside the wrapper.

_build_command() is called at subprocess_cli.py:812 — one line above the try: that begins at :813. So a JSONDecodeError from reading the settings file is not converted to CLIConnectionError and self._exit_error is never set. Callers get a json exception from what is otherwise a ClaudeSDKError hierarchy, naming a column offset and no file.

What I would change. Raise instead of degrading, for the inline-string case specifically, since it is not a documented degradation and it loses user configuration:

  • inline string that fails to parse → raise a domain error naming the parse failure, rather than reinterpreting it as a path;
  • keep the documented missing-file behaviour as it is, since that one is in the docstring;
  • move _build_command() inside the try, or wrap the file read, so the failure comes out as CLIConnectionError.

The one judgement call I would want your view on: whether "malformed inline string" should raise or should be added to the documented degradation list alongside the missing file. Raising is safer, but it is a behaviour change for anyone whose settings happen to contain a trailing comma today and who has not noticed that they are running with no settings at all.

I have not implemented any of this. Say which you want and I will open a PR.

Activity

  1. tanveer-arch commented on Oct 3, 2026

    @tanveer-arch

    I'd like to work on this. Two quick questions before I start: (1) do you accept external PRs on this repo? (2) For the fix, I'd parse the inline settings string strictly on malformed JSON, raise a clear error identifying it as the inline settings string (not a file), and make the file-form error name the file while keeping the documented missing-file degradation as-is. Is raising the behavior you'd want here, or would you prefer a fallback? Happy to implement whichever you choose, with regression tests covering the behavior matrix in the issue.

  2. feiiiiii5 commented on Oct 3, 2026

    @feiiiiii5
    Author

    Thanks — and yes to your first question: external PRs are welcome here, and this one is a good candidate precisely because you have already read the issue.

    I want to save you from duplicating work, so here is where I actually left it. I did not implement anything, as the issue says. What I did do was pin the reproduction to a specific commit and enumerate the behaviour matrix, which is the part that is slow to get right. At 37422c2, calling the real _build_settings_value():

    valid JSON + sandbox                   -> {"model": "sonnet", "sandbox": {"enabled": true}}
    MALFORMED + sandbox                    -> {"sandbox": {"enabled": true}}
    MALFORMED, no sandbox (pass-through)   -> {"model":"sonnet",}
    path to MALFORMED file + sandbox       -> RAISED JSONDecodeError: Illegal trailing comma before end of object
    nonexistent path + sandbox             -> {"sandbox": {"enabled": true}}
    

    with MALFORMED being '{"model":"sonnet",}'. Two details in there are worth having before you write the test, because they are what make the bug a bug rather than a preference:

    The silent case is the loud-looking one. With sandbox set, one trailing comma costs the entire settings object and the run proceeds looking healthy. Without sandbox, the same bytes pass through verbatim and the CLI rejects them outright. So the dangerous configuration is the quieter one — that inversion is the argument for raising rather than degrading.

    And the warning text at subprocess_cli.py:507-509 reports "treating as file path" while the "file path" is the JSON text itself. It never says the settings were dropped.

    On your question (2): your plan matches what I would choose, and I agree with the shape of it — raise for the inline case, keep the documented missing-file degradation, and make the file-form error name the file. The _build_command() placement is the other half: it is called at subprocess_cli.py:812, one line above the try: that starts at :813, so a JSONDecodeError from reading the settings file escapes the ClaudeSDKError hierarchy and never sets _exit_error. If you do both, note that the file-form path also needs the move, or the error shape stays inconsistent between the two forms you are deliberately distinguishing.

    I have no intention of opening a competing PR, so please go ahead. Two things that would help a reviewer on your side: if you can, say which commit you tested against, since this area has been refactoring; and the regression test is worth asserting on the warning as well as the raised error, because the warning naming a nonexistent file path is the part a reader would otherwise believe.

    If it would be more useful I can also review your PR when it is up — I have no commit rights to this repo's branch protections but I can run the matrix. Otherwise, good luck.

  3. tanveer-arch commented on Oct 3, 2026

    @tanveer-arch

    Thanks for the detailed spec implemented exactly per your direction and the PR is up: #1352. Strict inline parse, file-form error names the file, missing-file degradation kept, both paths moved inside the try: so errors stay in the ClaudeSDKError hierarchy. Tests assert on the warning contents too, and I stated the tested commit in the description. Would appreciate your review when you have a moment.

  4. feiiiiii5 commented on Oct 3, 2026

    @feiiiiii5
    Author

    Closing the loop on my side: I have reviewed #1352 at 755bd5a3 and left the detail there — short version, tests/test_transport.py is 209 passed, the wider suite is 1401 passed, ruff and mypy are clean, and reverting only the source file while keeping your tests gives 3 failures, so the regression tests genuinely pin the fix.

    Nothing blocking from me, and I have left the approval to whoever owns the repo.

    I am marking this as done from my end so the issue does not sit open waiting on me. If the maintainers send it back with changes, or if you want a second pass on a follow-up commit, say so and I will pick it up.

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