[BUG] Workspace trust is granted by ancestor walk but consumed by exact match for extraKnownMarketplaces, silently disabling project-scoped marketplace config

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 1 comment · opened Aug 6, 2026

Environment

  • Claude Code 2.1.222, native install, macOS 15 (darwin 25.5.0)

Summary

Workspace trust is granted by walking up the directory tree, but consumed by exact path match when deciding whether to honor extraKnownMarketplaces from project settings. The two disagree, so accepting the trust dialog once at a parent directory permanently and silently disables project-scoped marketplace configuration in every repository underneath it -- with no prompt, no warning, and no log line.

The asymmetry

Granting / prompt-gating walks ancestors:

function sUb(){
  if (te.CLAUDE_CODE_SANDBOXED) return true;
  if (nxe()) return true;
  if (vs()) return true;
  let e = Lt(), t = ove();
  if (e.projects?.[t]?.hasTrustDialogAccepted) return true;
  let n = X2e(Mt());
  while (true) {                                        // ancestor walk
    if (e.projects?.[n]?.hasTrustDialogAccepted) return true;
    let i = X2e(Cb.resolve(n, "..")); if (i === n) break; n = i;
  }
  return false;
}

(Cqe() does the same walk, with an exact-match probe first.)

Consumption for marketplaces is exact-match only:

function hF(e){ return uCr(e ?? pn()) }
function uCr(e){ let t = Gbn(e); return Lt().projects?.[t]?.hasTrustDialogAccepted === true }

function mne(){
  let r = Nwo();                                        // -> hF() -> uCr(), exact match
  let n = r ? co().extraKnownMarketplaces ?? {} : tB_();
  return { ...e, ...(r ? BCs() : {}), ...n }
}
function tB_(){
  for (let t of mw()) {
    if (eB_.has(t)) continue;                           // eB_ = {"projectSettings","localSettings"}
    ...
  }
}

Resulting failure

  1. A user accepts the trust dialog once at a parent directory -- ~/Projects, ~/src, ~/code. Common, and encouraged by the fact that the dialog appears per directory.
  2. In every repo underneath, sUb() returns true via the ancestor walk, so the trust dialog never appears again.
  3. Because it never appears, mvt() never writes hasTrustDialogAccepted for those child paths, and they retain the default false.
  4. Nwo() therefore evaluates false in every child repo, so mne() takes the tB_() branch, which excludes projectSettings and localSettings.
  5. Any extraKnownMarketplaces committed to a repo's .claude/settings.json is silently discarded.

Observed on this machine: ~/.claude.json has hasTrustDialogAccepted: true for /Users/<me>/Projects and false for /Users/<me>/Projects/<repo>. The false means "never asked", not "asked and declined" -- the user was never given the opportunity to trust the child path, and there is no UI affordance to request the prompt again.

Why it is hard to notice

A repo's committed .claude/settings.json is partially honored, which makes the failure look impossible:

  • committed hooks execute normally (verified: a SessionEnd hook in project settings ran to completion, status 0)
  • committed permissions.deny rules apply normally (verified in the debug log)
  • committed enabledPlugins is read normally
  • committed extraKnownMarketplaces is discarded

Hooks and permissions consult the ancestor-walking predicate; marketplaces consult the exact-match one. So "this directory is trusted" means two different things depending on which subsystem asks, from the same settings file.

The symptom is an enabledPlugins entry that can never resolve:

[DEBUG] Skipping orphaned enabledPlugins entry <plugin>@<marketplace>: marketplace not registered

That line names the plugin but not the cause. Nothing states that the marketplace declaration was dropped for trust reasons. There is a string in the bundle for a comparable case elsewhere ("Ignoring N entries from <file>: this workspace has not been trusted. Run Claude Code interactively here once and accept the trust dialog, or set projects[\"<path>\"].hasTrustDialogAccepted: true"), but it was never emitted in any of seven captured --debug-file logs across print, interactive, trusted-cwd and untrusted-cwd sessions -- so whatever it covers, it is not this path.

Requested change

Any of:

  1. Use the same ancestor-walking predicate for extraKnownMarketplaces that hooks and permissions already use, so one settings file is honored consistently.
  2. Or, if exact match is deliberate for marketplaces (a plausible security argument -- marketplace config causes code to be fetched and executed, so inheriting trust from an ancestor may be too permissive), then say so at runtime: emit the existing "not been trusted" diagnostic when marketplace entries are dropped, and re-prompt for the child path rather than silently skipping.
  3. At minimum, document that extraKnownMarketplaces is not read from project or local settings. The settings documentation lists it among project-settings keys with no such caveat, which is what led us to commit it to a shared repo and assume it worked.

Option 2 seems best: keep the stricter check, but make it observable and recoverable.

Reproduction

  1. mkdir -p ~/tmp-trust-demo/child && cd ~/tmp-trust-demo and start Claude Code; accept the trust dialog.
  2. cd child, add .claude/settings.json containing an extraKnownMarketplaces entry plus a matching enabledPlugins entry.
  3. Start Claude Code in child -- note that no trust dialog appears (ancestor grant).
  4. Observe the marketplace is never registered; known_marketplaces.json is unchanged; --debug shows only Skipping orphaned enabledPlugins entry ...: marketplace not registered, with no explanation.
  5. Set projects["<abs path to child>"].hasTrustDialogAccepted: true in ~/.claude.json and restart -- the marketplace now registers.

Step 3 is the crux: the user is never offered the trust decision that step 5 turns out to require.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗