claudewheel v0.20.1 /claudewheel.reconcile
Edit
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

python
def is_empty(self) -> bool

True when no additions or removals are needed (already canonical).

#change_count

python
def change_count(self) -> int

Total number of individual add/remove operations in this diff.

#_reconcile_list

python
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

python
def compute_settings_diff(container: dict) -> PermissionDiff

Compute 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

python
def apply_settings_diff(container: dict, diff: PermissionDiff) -> None

Mutate 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

python
def _print_diff(label: str, diff: PermissionDiff, dry_run: bool) -> None

Print a per-target diff header and one line per add/remove operation.

#run_reconcile

python
def run_reconcile(dry_run: bool, profile: str | None) -> int

Reconcile 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.