Skip to content

feat: auto-wire ctx hooks into .claude/settings.json on harness init#19

Merged
saldestechnology merged 1 commit into
mainfrom
feat/harness-auto-wire-settings
Jul 10, 2026
Merged

feat: auto-wire ctx hooks into .claude/settings.json on harness init#19
saldestechnology merged 1 commit into
mainfrom
feat/harness-auto-wire-settings

Conversation

@saldestechnology

Copy link
Copy Markdown
Collaborator

What

ctx harness init --mode local now merges the ctx hooks + permissions into .claude/settings.json itself, 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)

  • Missing file -> created from the snippet (Created).
  • Existing JSON object -> deep-merged into a clone (Merged):
    • permissions.allow / permissions.deny: union with dedup by value; existing entries kept in order, snippet entries appended only if absent. Arrays/permissions created 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.
    • Every other key is left untouched.
    • If nothing changed, the file is not rewritten -> AlreadyWired (re-init leaves 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 today's behavior: a stderr warning plus the snippet on stdout for manual merge (SkippedInvalid).

--json output gains a settings_action field (created / merged / already_wired / skipped_invalid); settings_snippet is retained.

Doctor's check_settings_wiring is unchanged (still valid for the manual-unwire / invalid-JSON cases).

Tests added

Unit (src/harness/mod.rs):

  • test_wire_creates_settings_when_missing
  • test_wire_merges_additively_preserving_user_settings
  • test_wire_is_idempotent
  • test_wire_leaves_invalid_json_untouched

CLI (tests/harness_cli.rs):

  • test_init_merges_into_existing_settings_additively
  • test_init_is_idempotent_on_settings
  • test_init_leaves_invalid_settings_untouched_and_prints_snippet
  • test_local_init_post_tool_use_hook_runs_end_to_end updated 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).
  • Manual smoke on a scratch dir: run 1 creates settings.json wiring the hooks; run 2 reports "already wired for ctx" with identical bytes; corrupting the file then re-running prints the warning + snippet and leaves the file byte-for-byte unchanged; ctx harness doctor reports no settings_not_wired.

`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.
@saldestechnology
saldestechnology merged commit 954ca62 into main Jul 10, 2026
5 checks passed
saldestechnology added a commit that referenced this pull request Jul 10, 2026
Auto-wire ctx hooks into .claude/settings.json on 'harness init'
(#19), plus internal Solidity/Go/TypeScript parser refactors
(#16, #17). Move Unreleased changelog entries to 0.3.1 and update
comparison links.
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