Document --print --settings: replace-vs-merge semantics
Summary
Documentation gap: claude --print --settings <fixture.json> semantics for the hooks block are not documented. Empirical testing across three independent probes shows it is whole-replace for hooks (with possible per-key behavior for other keys — three-way semantics), not per-key merge.
Outcome label
Outcome A — whole-replace (for the hooks block specifically).
Trace evidence shape
Three independent probes, all spawning claude --print with --settings <fixture> and reading the resulting ~/.claude/traces/<sid>.jsonl:
| Probe | Fixture contents | Trace lines | hook substring matches (case-insensitive) | Verdict |
|---|---|---|---|---|
| 1 (2026-05-05) | permissions only, no hooks key | small | 0 | hooks did not fire |
| 2 (2026-05-06 early) | permissions only, no hooks key | small | 0 | hooks did not fire |
| 3 (2026-05-06 today) | permissions + verbatim copy of all 16 production hooks event keys | 13 | 0 | hooks STILL did not fire |
Probe 3's fixture (9569 bytes) included PreToolUse, PostToolUse, PostToolUseFailure, SessionStart, SubagentStart, PreCompact, PostCompact, InstructionsLoaded, SubagentStop, TeammateIdle, Stop, Notification, CwdChanged, FileChanged, PermissionRequest, UserPromptSubmit — the complete production hooks block, byte-for-byte. None fired. The 13 trace lines were all bootstrap.lifecycle.auto_load events emitted by the harness itself.
Verification command (verbatim):
TRACE=~/.claude/traces/<probe_sid>.jsonl
LINES=$(wc -l < "$TRACE") # 13
HOOK_LINES=$(grep -c -i hook "$TRACE") # 0
[ "$HOOK_LINES" -ge 1 ] && echo per-key-replace || echo whole-replace
# whole-replace
Three-way semantics refinement (worth documenting too)
The probe's system.init line in --output-format json showed that the parent session's output_style: \"Explanatory\" and full memory_paths block leaked into the spawned session — even though the fixture did not specify those. So some config keys cross over while hooks is fully replaced. Either:
--settingsis whole-replace AND the leaked keys come from a different layer (user-level vs project-level resolution), or--settingsis per-key-replace EXCEPT for thehooksblock which has special handling.
Either way, the documented mental model breaks for users trying to test hook configurations via --settings.
Why this matters for our use case
We're building automated tests for our hook system that need to spawn a fresh claude --print with a custom hooks config. The natural pattern — \"copy production hooks into the fixture, exercise the test\" — does not work because the hooks block is fully replaced and hooks do not fire. Without a documented merge semantic, we cannot:
- Run integration tests against custom hook configurations
- Build deterministic burst-launch sentinels for SessionStart timing
- Exercise hook scripts in isolation from production state
What would help
- Document the current semantics explicitly (replace, merge, per-key behavior).
- If there's an undocumented way to make hooks fire from a fixture, document it.
- Long-term: a
--settings-merge <fixture>mode that does deep-merge against the production settings would unblock the test patterns above.
Internal references
- Source measurement: ticket #561 (yuna, 2026-05-05) — original AC-B4 escalation, fixture without hooks.
- Spec anticipating this fork: ticket #544 §AC2 (\"open question: does
--settings <fixture>replace the production settings completely...\"). - This refinement: ticket #576 (jahi, 2026-05-06) — fixture WITH hooks copied verbatim, same outcome.
- Audit trail:
D:/VersionControl/claude-env/docs/plans/protocol-os/audits/sessionstart-silence-investigation.md§5.3.x.
Environment
- Claude Code 2.1.126
- Windows 11 Pro 10.0.26200, Git Bash via Windows Terminal
- Probe model: claude-haiku-4-5-20251001
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗