@@ -522,23 +522,42 @@ def _extract_caller_settings(tool_args: list[str]) -> tuple[list[str], list[str]
522522 return values , remaining
523523
524524
525- def _load_caller_settings (value : str ) -> dict | None :
525+ def _load_caller_settings (value : str ) -> dict :
526526 """Resolve a ``--settings`` value (inline JSON or file path) to a dict.
527527
528- Returns ``None`` when it is neither parseable JSON nor an existing file, so
529- the caller can pass such a value through untouched instead of dropping it.
528+ Claude Code accepts either inline JSON or a path to a JSON file. Raises
529+ ``RuntimeError`` (surfaced by the CLI as an actionable error) when the value
530+ is neither, rather than silently dropping it: a dropped value would also be
531+ passed through as a second ``--settings`` flag, and Claude Code honors only
532+ one — so either the caller's settings or ucode's gateway config would be
533+ silently ignored. Failing loudly lets the caller fix their input.
530534 """
531535 text = value .strip ()
532536 if text .startswith ("{" ):
537+ source , malformed = text , "value is not valid JSON"
538+ else :
539+ path = Path (text )
540+ if not path .exists ():
541+ raise RuntimeError (
542+ f"--settings file not found: { value !r} . "
543+ "Pass inline JSON or a path to an existing JSON file."
544+ )
533545 try :
534- parsed = json .loads (text )
535- except json .JSONDecodeError :
536- return None
537- return parsed if isinstance (parsed , dict ) else None
538- path = Path (text )
539- if path .exists ():
540- return read_json_safe (path )
541- return None
546+ source = path .read_text (encoding = "utf-8" )
547+ except OSError as exc :
548+ raise RuntimeError (f"--settings file could not be read: { value !r} ({ exc } )." ) from exc
549+ malformed = "file is not valid JSON"
550+ try :
551+ parsed = json .loads (source )
552+ except json .JSONDecodeError as exc :
553+ raise RuntimeError (
554+ f"--settings { malformed } ({ exc } ): { value !r} . Pass inline JSON or a path to a JSON file."
555+ ) from exc
556+ if not isinstance (parsed , dict ):
557+ raise RuntimeError (
558+ f"--settings must be a JSON object, got { type (parsed ).__name__ } : { value !r} ."
559+ )
560+ return parsed
542561
543562
544563def _union_claude_hooks (base : dict , overlay : dict ) -> dict :
@@ -589,30 +608,22 @@ def _build_claude_argv(binary: str, tool_args: list[str]) -> list[str]:
589608 keys win, hooks from both are unioned — and hand Claude a single merged
590609 ``--settings`` (inline JSON). The merge is per-launch and is never written
591610 back to the shared ucode settings file, so concurrent launches cannot
592- accumulate one another's hooks.
611+ accumulate one another's hooks. A caller ``--settings`` value ucode cannot
612+ resolve raises (see :func:`_load_caller_settings`) rather than being passed
613+ through as a second, colliding flag.
593614 """
594615 caller_values , remaining = _extract_caller_settings (tool_args )
595616 if not caller_values :
596617 # No caller --settings: hand Claude ucode's settings file directly (the
597618 # common path; behavior unchanged).
598619 return [binary , "--settings" , str (CLAUDE_SETTINGS_PATH ), * tool_args ]
599620 caller_settings : dict = {}
600- unparsed : list [str ] = []
601621 for value in caller_values :
602- parsed = _load_caller_settings (value )
603- if parsed is None :
604- unparsed .append (value )
605- continue
606- caller_settings = _merge_claude_settings (caller_settings , parsed )
622+ caller_settings = _merge_claude_settings (caller_settings , _load_caller_settings (value ))
607623 # ucode wins over the caller for conflicting keys (protects gateway auth);
608624 # hooks from both sides survive.
609625 merged = _merge_claude_settings (caller_settings , read_json_safe (CLAUDE_SETTINGS_PATH ))
610- argv = [binary , "--settings" , json .dumps (merged , separators = ("," , ":" )), * remaining ]
611- # Anything we could not parse (not JSON, not an existing file) is passed
612- # through untouched rather than silently dropped.
613- for value in unparsed :
614- argv .extend (["--settings" , value ])
615- return argv
626+ return [binary , "--settings" , json .dumps (merged , separators = ("," , ":" )), * remaining ]
616627
617628
618629def launch (state : dict , tool_args : list [str ]) -> None :
0 commit comments