affaan-m/ECC · warning · Error

Refusing to overwrite unowned JSON fields at ${destinationPa

Error message

Refusing to overwrite unowned JSON fields at ${destinationPath}: ${conflicts.join(', ')}

What it means

Thrown by classifyManagedOperation for a merge-json operation on an unowned destination when findJsonConflicts detects at least one field where the patch wants to set a value that differs from the existing value. ECC will only add fields that are absent (or overwrite fields it owns); overwriting fields the user set themselves is destructive, so the conflicting field paths are listed and the operation aborts. Nested objects are recursed; matching values are not conflicts.

Source

Thrown at scripts/lib/multi-harness-setup.js:228

    const currentValue = current[key];
    const field = prefix ? `${prefix}.${key}` : key;
    if (isPlainObject(currentValue) && isPlainObject(patchValue)) {
      return findJsonConflicts(currentValue, patchValue, field);
    }
    return JSON.stringify(currentValue) === JSON.stringify(patchValue) ? [] : [field];
  });
}

function classifyManagedOperation(operation, ownedDestinations) {
  const destinationPath = operation.destinationPath;
  if (!fs.existsSync(destinationPath)) return 'create';
  const canonicalDestination = canonicalPath(destinationPath);
  if (operation.kind === 'merge-json') {
    const current = assertMergeDestination(destinationPath);
    if (ownedDestinations.has(canonicalDestination)) return 'managed-json-update';
    const conflicts = findJsonConflicts(current, operation.mergePayload);
    if (conflicts.length > 0) {
      throw new Error(
        `Refusing to overwrite unowned JSON fields at ${destinationPath}: ${conflicts.join(', ')}`
      );
    }
    return 'json-merge';
  }
  if (ownedDestinations.has(canonicalDestination)) return 'managed-update';
  if (
    operation.kind === 'copy-file'
    && typeof operation.sourcePath === 'string'
    && fs.existsSync(operation.sourcePath)
    && fs.statSync(destinationPath).isFile()
    && fs.readFileSync(operation.sourcePath).equals(fs.readFileSync(destinationPath))
  ) {
    return 'identical';
  }
  throw new Error(`Refusing to replace unowned existing file: ${destinationPath}`);
}

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Read the conflicting paths in the message and reconcile: change your config to match ECC's value, or change ECC's mergePayload (profile) to match yours.
  2. If you want ECC to take ownership of that file, mark the destination as managed first (run a prior install that records ownership), then the merge becomes a managed-json-update.
  3. Remove the conflicting key from your config so ECC adds it fresh.
  4. Choose a different profile whose patch does not touch your customized keys.

Example fix

// before: user has settings.json { "hooks": { "PreToolUse": "mine" } }
// ECC merge-json patch: { "hooks": { "PreToolUse": "ecc-hook" } }
classifyManagedOperation(op, owned); // throws [291]: hooks.PreToolUse

// after (option A): align your value to ECC's
// settings.json: { "hooks": { "PreToolUse": "ecc-hook" } }
// after (option B): let ECC own the file by installing once first so it is in ownedDestinations
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
function isPlainObject(v){return Boolean(v)&&typeof v==='object'&&!Array.isArray(v);}
function findConflicts(current, patch, prefix='') {
  if (!isPlainObject(patch)) return [];
  return Object.entries(patch).flatMap(([k, pv]) => {
    if (!Object.prototype.hasOwnProperty.call(current, k)) return [];
    const cv = current[k];
    const f = prefix ? `${prefix}.${k}` : k;
    if (isPlainObject(cv) && isPlainObject(pv)) return findConflicts(cv, pv, f);
    return JSON.stringify(cv) === JSON.stringify(pv) ? [] : [f];
  });
}
function assertNoUnownedConflicts(destinationPath, mergePayload) {
  if (!fs.existsSync(destinationPath)) return;
  const conflicts = findConflicts(JSON.parse(fs.readFileSync(destinationPath,'utf8')), mergePayload);
  if (conflicts.length) throw new Error(`Conflicts at ${destinationPath}: ${conflicts.join(', ')}`);
}
assertNoUnownedConflicts(operation.destinationPath, operation.mergePayload);

Type guard

null

Try / catch

try {
  classifyManagedOperation(operation, ownedDestinations);
} catch (err) {
  if (/Refusing to overwrite unowned JSON fields/.test(err.message)) {
    // surface conflicting paths so the user can reconcile, or mark the file as managed first
    throw err;
  }
  throw err;
}

Prevention

When it happens

Trigger: Fires when ownedDestinations does not contain the canonical destination and findJsonConflicts(current, operation.mergePayload) returns a non-empty list. Each entry is a dotted path where current[path] exists, both sides are non-objects (or type-mismatched), and JSON.stringify(currentValue) !== JSON.stringify(patchValue). Typical when the user already configured a setting ECC's patch also tries to set with a different value.

Common situations: User set 'hooks' or 'permissions' in settings.json with their own value; an existing tsconfig compilerOptions value differs from ECC's recommendation; two ECC modules try to write the same key differently; profile merge payload changed a default the user customized.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/79422f93b2aa5a6a. Report an issue: GitHub.