Skip to content

Stop Claude runs inheriting the host's user settings via --setting-sources - #2

Merged
yasyf merged 1 commit into
mainfrom
claude-setting-sources
Sep 2, 2026
Merged

yasyf merged 1 commit into
mainfrom
claude-setting-sources

Conversation

@yasyf

@yasyf yasyf commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

ClaudeConfig had no way to reach the claude CLI's --setting-sources, so every spawn with use_host_config: true inherited the host's user-level settings, hooks included, and nothing could turn them off. On a machine with a real hook stack the hooks dominate the call.

Measured on macOS with an identical one-word prompt to claude-haiku-4-5-20251001, MCP already disabled via --strict-mcp-config --mcp-config '{"mcpServers":{}}':

invocation wall
as spawnllm built it before this change 63.0s, exiting with SessionEnd hook … failed: Hook cancelled
--bare 2.4s, but Not logged in: it skips keychain reads, so subscription auth cannot use it
--setting-sources project 6.4 / 7.5 / 5.7s, correct answer, auth intact
--settings '{"hooks":{}}' 44.7s: settings merge, so this does not remove hooks

The downstream victim was slop-cop, whose per-chunk lint calls each paid that cost and blew their timeouts.

The change

setting_sources: Option<Vec<String>> joins ClaudeConfig, resolved at one site in rust/spawnllm-core/src/plan/claude.rs:

  • an explicit list joins with commas and wins over everything, isolation included;
  • unset with isolated keeps emitting --setting-sources "", so every isolated invocation is argv-identical to before;
  • unset with host config now emits --setting-sources project. This is the new default and the point of the change.

It is a list passthrough rather than a boolean on purpose. CodexConfig's enable_hooks/enable_mcp are booleans because codex's knob is one (-c features.hooks=false); --setting-sources is a list that governs more than hooks, and the nil-vs-empty Option<Vec<String>> shape is the one tools and disallowed_tools already use.

Every layer carries it, by hand (the bindings have no generator): the core wire type and plan, the Rust crate's mirrored spec, Go (SettingSources []string on ClaudeConfig, the wire struct, and coreClaudeOf), and Python (setting_sources on ClaudeConfig plus a new TSettingSource literal alias). The Python SDK backend previously hardcoded ["user", "project", "local"] for non-isolated runs; it now follows the same resolution as the CLI plan.

Conformance: 16 existing claude vectors change, but only two change argv, claude-isolated-false and claude-strict-mcp, the two non-isolated cases, which both gain --setting-sources project. The other 14 are isolated and only gain "setting_sources": null in their input. Three new vectors cover the explicit restore (user,project,local), an empty list resolving to "", and an explicit list beating isolation.

Breaking change

Anyone relying on use_host_config: true to pick up user-level settings loses them. MCP servers declared in ~/.claude/settings.json, user-level permissions, and hooks all stop loading for non-isolated runs.

To restore the previous behavior, name the sources explicitly:

  • Python: ClaudeConfig(setting_sources=("user", "project", "local"))
  • Go: ClaudeConfig{SettingSources: []string{"user", "project", "local"}}
  • Rust: setting_sources: Some(vec!["user".into(), "project".into(), "local".into()])

--settings is not a workaround. Settings merge rather than replace, so --settings '{"hooks":{}}' leaves the inherited hooks in place; that invocation still measured 44.7s above.

The CHANGELOG entry under [Unreleased] / Changed describes the same default change and restore path.

Test plan

Green:

  • Rust core: 32 unit tests, conformance replay (3), freshness (1); cargo fmt --check; clippy -D warnings; cargo test -p spawnllm --no-default-features
  • Go: go vet, go build, go test -race ./...
  • Python: pytest 415 passed with 2 platform skips; ruff check and ruff format; ty at its 21-diagnostic baseline
  • Docs build 19/19

Not green, and shown pre-existing by running the same commands against an unmodified HEAD in a separate worktree:

  • Two rust/spawnllm/tests/integration.rs cases, call_returns_text_via_auto_selected_backend and run_on_times_out_and_kills_the_child, fail at baseline too. They want real authenticated CLIs and hold 10s wall-clock deadlines.
  • TestSelectBackendSpecialtyPromotes flaked once under -race on a loaded machine and passes on an idle one.
  • golangci-lint is not installed on this machine, so task go:lint did not run.

https://claude.ai/code/session_014gKfZvjihGgFrEVCr3NJJb

@yasyf
yasyf force-pushed the claude-setting-sources branch from 4c121e0 to aa72110 Compare September 2, 2026 21:45
@yasyf
yasyf merged commit 4157275 into main Sep 2, 2026
17 checks passed
@yasyf
yasyf deleted the claude-setting-sources branch September 2, 2026 21:48
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.

1 participant