Skip to content

fix(host-contracts): guard Claude's .claude.json by its user-scope mcpServers only in the native smoke - #529

Merged
ScriptedAlchemy merged 3 commits into
mainfrom
fix/439-claude-normal-home-guard
Sep 4, 2026
Merged

fix(host-contracts): guard Claude's .claude.json by its user-scope mcpServers only in the native smoke#529
ScriptedAlchemy merged 3 commits into
mainfrom
fix/439-claude-normal-home-guard

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Fixes #439.

Root cause

snapshotClaudeNormalHome (packages/agent-bundle/src/host-contracts/native-claude-contract.ts:337-343 before this PR) digested $HOME/.claude.json whole (claudeJson) beside config.json, settings.local.json, plugins/, and settings.json, and sameClaudeNormalHome compared all five. Claude Code rewrites .claude.json on every signed-in start, so a smoke whose own evidence was clean still ended in claude-native.normal-home.changedharness-failure.

Verified read-only on this machine (Claude Code 2.1.260):

  • The real ~/.claude.json has 81 top-level keys, all host bookkeeping (cachedGrowthBookFeatures*, cachedExperiment*, firstStartTime, machineID, userID, migrationVersion, seenNotifications, pluginUsage, skillUsage, numStartups, projects.<cwd>.last*, …) and no mcpServers. Over a 90 s read-only probe its whole-file SHA-256 changed while the mcpServers digest stayed none.
  • In a fresh isolated HOME (signed out) claude -p creates .claude.json on the first start (firstStartTime, firstStartVersion, machineID, migrationVersion, seenNotifications, userID, …) — so the old guard also tripped for any fresh-home run (absent → digest). Two consecutive signed-out runs left it byte-identical; the per-turn rewrites are the signed-in path the audit recorded on 2.1.257/2.1.259.
  • The CI pin (2.1.250, PROVENANCE.json) only gates the login-free proofs; native-host-smoke.yml runs the signed-in leg against whatever CLI the trusted runner has, which is where this tripped.

Fix

The guard keeps digesting the settings files and the plugins/ tree whole, and replaces claudeJson with stateMcpServers: the canonical digest (core/digest) of .claude.json's top-level mcpServers only — the one durable configuration in that file a plugin smoke could plausibly alter (claude mcp add -s user). An absent file and an absent key both digest to none (a first start creates the file without registrations); a file that is not a JSON object digests to unparsable, so corrupting it is still a change. The diagnostic message now names "user-scope MCP registrations" beside config/settings/plugins. This matches the surface normalClaudeSettingsAndPluginsUnchanged (host-install session proofs, packed native smoke) already guards, plus the registrations.

Tests

native-claude-contract.test.ts: the old protects the default sibling Claude state file without retaining its opaque contents (which asserted the opposite) is replaced by two cases — a signed-in bookkeeping rewrite (the observed keys) and a first start creating the file both pass with normalHome: 'unchanged' and leak no marker; adding, changing, or removing a mcpServers registration, or corrupting the file, still fails with exactly claude-native.normal-home.changed. pnpm test:unit 3246 passed; pnpm typecheck, pnpm lint clean. No user-facing docs mention this guard (docs/diagnostics.md does not list claude-native.* codes), so the changeset carries the scope change.

Review status

  • Codex review of d878d10 opened one P2 thread (native-claude-contract.ts:351: the security reference en/zh and the README still promised failure on any normal-home change). Addressed in d8c04a0a4 (59bcd840e after rebase): all three now name the exact surface — ~/.claude/config.json, settings.json, settings.local.json, plugins/, plus the user-scope mcpServers of ~/.claude.json for the source contract smoke — and state that the rest of .claude.json is host bookkeeping deliberately not guarded. pnpm docs:site:build clean. No reply was posted on the thread by request; this note is the response.
  • Last Codex-reviewed head: d878d10d9. Unreviewed heads: the changeset PR-number commit, the docs commit, and the rebases onto main after fix(routes): follow re-exported default components in the route contract check (AB4810) #524/fix(skills): serve agent-bundle/meta to rendered skills and compile their JSX without the consumer react runtime #527 (59bcd840e). No @codex review was requested by instruction (no PR comments); merged on green CI per the reviewer-fallback rule.

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 59bcd84

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T05:55:31.688318Z d878d10 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d878d10d9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +350 to +351
* Digests the user-scope `mcpServers` registrations of Claude's `.claude.json`
* and nothing else in it. An absent file and a file without the key both mean

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the narrowed normal-home guarantee

After this narrowing, modifications to any top-level .claude.json field other than mcpServers intentionally pass, while website/docs/en/reference/security.mdx:21-23, website/docs/zh/reference/security.mdx:17-20, and packages/agent-bundle/README.md:1058-1059 still tell users that contributor smoke commands fail whenever the selected host's normal home state changes. Update both locale pages and the README to describe the exact config/settings/plugins and user-scope MCP surface; otherwise the documented security guarantee is stronger than the implementation.

AGENTS.md reference: AGENTS.md:L71-L77

Useful? React with 👍 / 👎.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@529
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@529
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@529

commit: 59bcd84

@ScriptedAlchemy
ScriptedAlchemy force-pushed the fix/439-claude-normal-home-guard branch from 435aef3 to 5f34d52 Compare September 4, 2026 05:57
…pServers only in the native smoke

snapshotClaudeNormalHome digested the whole sibling state file, which Claude
Code 2.1.257+ rewrites on every signed-in start (cached feature flags,
first-start/machine identity, usage counters, per-project statistics), so a
passing smoke reported claude-native.normal-home.changed. The guard now keeps
the settings files and plugins/ tree whole and reads only the mcpServers
registrations out of .claude.json; absent file and absent key both mean no
registrations, a corrupt file digests to its own constant.

Fixes #439
Review: the security reference (en/zh) and the README said the smokes fail
whenever the host's normal home state changes; name the config/settings/
plugins surface and the user-scope mcpServers of .claude.json instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

native-claude-contract: normal-home guard digests .claude.json, which Claude Code 2.1.257 rewrites on every signed-in turn

1 participant