On this page
Reconcile profile and shared-settings permissions toward the canonical model.
#claudewheel.reconcile
#claudewheel.reconcile
Reconcile profile and shared-settings permissions toward the canonical model.
Where patch_profiles additively syncs hooks and disallowedTools, this module owns the permissions arrays. It brings every discovered profile's settings.json -- and shared-settings.json's profileDefaults -- into exact agreement with the canonical guardrail model in guardrail.py:
- permissions.deny is made to contain exactly canonical_deny_rules(): missing canonical entries are added, and any deny entry NOT in the canonical set is removed. - permissions.ask is reconciled identically against canonical_ask_rules(). - permissions.allow has every entry listed in ALLOW_CONFLICTS removed (dead or conflicting allow rules); all other allow entries are left alone and nothing is ever added to allow.
Unlike patch-profiles, reconciliation is NOT purely additive for deny/ask: it prunes drift. It never touches hooks, disallowedTools, or any non-permission key. All writes go through permission.py's atomic save_settings.
#PermissionDiff
The additions and removals needed to reconcile one permissions block.
#is_empty
def is_empty(self) -> boolTrue when no additions or removals are needed (already canonical).
#change_count
def change_count(self) -> intTotal number of individual add/remove operations in this diff.
#_reconcile_list
def _reconcile_list(current: list[str], canonical: list[str]) -> tuple[list[str], list[str]]Compute (to_add, to_remove) so current becomes exactly canonical.
to_add preserves canonical order (missing canonical entries in the order they appear in the model). to_remove preserves current order (entries present now but absent from the canonical set).
#compute_settings_diff
def compute_settings_diff(container: dict) -> PermissionDiffCompute the reconciliation diff for a dict holding a permissions block.
container is either a profile settings.json dict or a profileDefaults dict -- both nest their arrays under permissions. A missing permissions block (or missing arrays) is treated as empty. The allow array is only inspected when present; nothing is ever added to allow.
#apply_settings_diff
def apply_settings_diff(container: dict, diff: PermissionDiff) -> NoneMutate container in place to enact diff via the permission primitives.
Removals run before additions. Uses permission.add_rule (append-only) and permission.remove_rule so JSON IO and the permissions-block shape stay consistent with the rest of the codebase.
#_print_diff
def _print_diff(label: str, diff: PermissionDiff, dry_run: bool) -> NonePrint a per-target diff header and one line per add/remove operation.
#run_reconcile
def run_reconcile(dry_run: bool, profile: str | None) -> intReconcile permissions across profiles and shared-settings profileDefaults.
When profile is None, every discovered profile is reconciled AND shared-settings.json's profileDefaults is reconciled (so future profiles seed from a canonical baseline). When profile names a single profile, ONLY that profile is touched -- shared-settings is left alone, on the principle that scoping to one profile is a targeted operation and the shared baseline is a fleet-wide concern.
With dry_run, prints the diff for each target and writes nothing. Returns 0 on success, 1 if a named profile is not found.