feat: auto-wire ctx hooks into .claude/settings.json on harness init#19
Merged
Merged
Conversation
`ctx harness init --mode local` now merges the ctx hooks + permissions into `.claude/settings.json` itself instead of only printing a snippet for manual paste. Hooks go live on the next Claude Code session start without a manual merge step. The merge is additive and idempotent, and never clobbers unrelated user settings: - a missing settings file is created from the snippet; - an existing JSON object has ctx's `permissions.allow` / `permissions.deny` entries unioned (dedup by value, existing order preserved) and ctx's hook groups appended to each event only when no group there already references `.claude/hooks/ctx/` (the same marker the doctor uses); every other key is left untouched; - when nothing needs adding, the file is not rewritten (reported already-wired), so re-running init leaves the bytes identical. Fallback safety: if `.claude/settings.json` exists but is not valid JSON (or not an object), it is left byte-for-byte untouched and init falls back to the old behavior -- a stderr warning plus the snippet on stdout for manual merge. This intentionally reverses the prior "ctx does not edit settings.json" contract; the deny rule `Edit(.claude/settings.json)` still guards the file against later model edits. Tests: unit tests for the merge helper (create / additive-merge / idempotent / invalid-JSON) and CLI tests covering merge into pre-existing settings, idempotent re-init, and the invalid-JSON fallback. The end-to-end local-init test now asserts settings.json is written and valid. `--json` output gains a `settings_action` field.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
ctx harness init --mode localnow merges the ctx hooks + permissions into.claude/settings.jsonitself, instead of only printing a snippet the user had to paste by hand. Previously the hooks never fired until the user manually merged the snippet; now they go live on the next Claude Code session start.Behavior change
This intentionally reverses the prior "ctx does not edit settings.json" contract. The old prose ("Merge this snippet into .claude/settings.json (ctx does not edit it)") is gone. The
Edit(.claude/settings.json)deny rule still guards the file against later model edits.Merge semantics (additive + idempotent, never clobbers)
Created).Merged):permissions.allow/permissions.deny: union with dedup by value; existing entries kept in order, snippet entries appended only if absent. Arrays/permissionscreated if missing.hooks.SessionStart/hooks.PostToolUse/hooks.Stop: ctx's group appended to an event only if no existing group there references the substring.claude/hooks/ctx/(the same idempotency marker the doctor uses). Events created if missing.AlreadyWired(re-init leaves bytes identical).Fallback safety
If
.claude/settings.jsonexists but is not valid JSON (or not an object), it is left byte-for-byte untouched and init falls back to today's behavior: a stderr warning plus the snippet on stdout for manual merge (SkippedInvalid).--jsonoutput gains asettings_actionfield (created/merged/already_wired/skipped_invalid);settings_snippetis retained.Doctor's
check_settings_wiringis unchanged (still valid for the manual-unwire / invalid-JSON cases).Tests added
Unit (
src/harness/mod.rs):test_wire_creates_settings_when_missingtest_wire_merges_additively_preserving_user_settingstest_wire_is_idempotenttest_wire_leaves_invalid_json_untouchedCLI (
tests/harness_cli.rs):test_init_merges_into_existing_settings_additivelytest_init_is_idempotent_on_settingstest_init_leaves_invalid_settings_untouched_and_prints_snippettest_local_init_post_tool_use_hook_runs_end_to_endupdated to assert settings.json is written and valid (old "snippet printed" assertions removed).Verification
cargo fmt,cargo test --verbose(all pass),cargo clippy -- -D warnings(clean).ctx harness doctorreports nosettings_not_wired.