On this page
Wizard hook-merge helper plus a thin delegate to the unified reconcile core.
#claudewheel.patch_profiles
#claudewheel.patch_profiles
Wizard hook-merge helper plus a thin delegate to the unified reconcile core.
The old additive, user-extras-preserving profile sync (sync_profile_settings / sync_shared_settings / run_patch_profiles) has been REPLACED by the unified reconcile core in reconcile.py, which prunes each target's guardrail sections to EXACTLY canonical. run_patch_profiles here is now a thin wrapper around that core -- the claudewheel patch-profiles command and claudewheel reconcile-permissions command do the same thing.
merge_hooks (and its _script_basename helper) remain because the wizard uses them to assemble a NEW profile's hooks from a canonical base plus any clone-source hooks at creation time. That is profile construction, distinct from the reconcile core's pruning of existing profiles.
#_script_basename
def _script_basename(command: str) -> strReturn the trailing script name of a hook command path (or "").
#_list_at
def _list_at(container: dict[str, Any], key: str) -> list[Any]Return container[key] as a list; raise when it is not one.
The array counterpart of the reconcile core's _dict_at, and it follows the same rule: a MISSING key is created empty and returned (absence is ordinary bootstrap), an existing list is returned as is, and a present-but-non-list value raises :class:MalformedSettingsError naming the key and the type found. setdefault alone would hand the malformed value straight to .append and crash with a bare AttributeError naming neither the key nor the file. container is left exactly as it was: a value nobody can interpret is never silently repaired.
#merge_hooks
def merge_hooks(existing: dict[str, Any], canonical: dict[str, Any]) -> list[str]Merge canonical hooks into existing (mutated in place).
Canonical entries are matched to existing ones by their "matcher" field. Individual canonical hooks are matched to existing ones by script basename:
- a canonical hook whose basename is absent is APPENDED;
- a canonical hook whose basename is present but whose command points at a
DIFFERENT (stale) absolute path is REPATHED in place to the canonical command -- this is how a workspace relocation is healed, so a profile whose hook commands reference an old scripts directory is brought to the current one without duplicating the entry.
Only claudewheel-managed wirings (those in canonical) are ever touched; user-custom, non-canonical hooks are matched by neither basename nor matcher and so are preserved exactly. Returns human-readable descriptions of every hook added or repathed.
A malformed existing value raises :class:MalformedSettingsError naming the key and the type found: an event whose value is not an array ("PreToolUse": null), or a matched entry whose "hooks" is not one. Hand-edited settings are reported, never crashed on and never silently skipped past.
Used by the wizard to assemble a new profile's hooks. Existing profiles are reconciled to exact canonical by the reconcile core, not by this merge.
#run_patch_profiles
def run_patch_profiles(ws: 'Workspace', dry_run: bool=False) -> intReconcile every managed profile and shared-settings.json to exact canonical.
Thin delegate to the unified reconcile core. This PRUNES each target's guardrail sections (hooks, disallowedTools, permissions deny/ask and the canonical settings keys) to exactly canonical -- the old additive, user-extras-preserving behavior is gone.