fix(transport): guard against None allowed_tools in _apply_skills_defaults#963
fix(transport): guard against None allowed_tools in _apply_skills_defaults#963Aispotlightlab wants to merge 1 commit into
Conversation
`SubprocessCLITransport._apply_skills_defaults` calls `list(self._options.allowed_tools)` unconditionally. While the type annotation declares `list[str]` with `default_factory=list`, the `ClaudeAgentOptions` dataclass does not validate the attribute at runtime — wrapper libraries can (and do) assign `None` to express "no restriction". Real-world impact: `claude-code-telegram` sets `options.allowed_tools = None` when `DISABLE_TOOL_VALIDATION=true` (its recipe for adding third-party MCP servers). Every connect attempt then dies with `TypeError: 'NoneType' object is not iterable` inside `_build_command` — before the CLI even starts — and the failure surfaces to end users as the opaque `Unexpected error in Claude SDK`. Reproduced on 0.1.81 and 0.2.82. Fix: `list(... or [])`. An empty list is the natural "no restriction" sentinel and is already handled correctly downstream (line 256 only emits `--allowedTools` when the list is truthy). Added a regression test that fails before the patch and passes after.
kiwigitops
left a comment
There was a problem hiding this comment.
Quick clarifying question on the semantics when options.skills is also set. With this change, if a caller passes allowed_tools=None (meaning "no restriction") AND e.g. skills="all" or skills=["foo"], then _apply_skills_defaults coerces None → [] and appends "Skill" / "Skill(foo)" to that empty list. The resulting effective_allowed_tools is non-empty, so the truthy check at line 256 fires and --allowedTools Skill (or Skill(foo)) is passed to the CLI — which is a restrictive allowlist of one tool rather than "no restriction."
Is that the intended interaction for the claude-code-telegram use case (where the wrapper sets DISABLE_TOOL_VALIDATION=true and presumably wants all tools available, including skills), or would callers passing None typically not be using skills= at the same time? If the latter, it might be worth a one-line comment in _apply_skills_defaults noting that the or [] path produces a restrictive allowlist when skills is set, so future readers don't read None-means-"no restriction" too literally. Happy to be told this is out of scope for a one-line defensive guard.
Summary
SubprocessCLITransport._apply_skills_defaultscallslist(self._options.allowed_tools)unconditionally (subprocess_cli.py:196). While the type annotation declareslist[str]withdefault_factory=list, theClaudeAgentOptionsdataclass does not validate the attribute at runtime — wrapper libraries can (and do) assignNoneto express "no restriction".When that happens, every
ClaudeSDKClient.connect()dies withTypeError: 'NoneType' object is not iterablebefore the CLI even starts, and the failure surfaces as the opaqueUnexpected error in Claude SDKlog line.Reproduced on both 0.1.81 and 0.2.82 (current latest).
Reproduction
Traceback:
Real-world impact
claude-code-telegram (a popular wrapper, ~v1.6.0) does exactly this when its
DISABLE_TOOL_VALIDATION=trueis set — which is the standard recipe in its docs for adding third-party MCP servers (MemPalace, Mem0, custom MCPs, etc.). Every Telegram message then fails. Companion PR on their side: RichardAtCT/claude-code-telegram#206 — but a defensive guard in the SDK protects every other downstream wrapper too.Fix
An empty list is the natural "no restriction" sentinel and is already handled correctly downstream — line 256 only emits
--allowedToolswhen the list is truthy.disallowed_toolsis already None-safe (line 265 uses a truthy check), so it needs no change.Test
Added
test_build_command_allowed_tools_noneintests/test_transport.pythat:TypeErrorabove--allowedToolsflag is emitted🤖 Generated with Claude Code