Document --print --settings: replace-vs-merge semantics

Resolved 💬 1 comment Opened May 6, 2026 by hanuele Closed Jun 3, 2026

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:

  1. --settings is whole-replace AND the leaked keys come from a different layer (user-level vs project-level resolution), or
  2. --settings is per-key-replace EXCEPT for the hooks block 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

  1. Document the current semantics explicitly (replace, merge, per-key behavior).
  2. If there's an undocumented way to make hooks fire from a fixture, document it.
  3. 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

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗