Skip to content

Plugin skill identity: a plugin's skill is listed under its own name whatever skillOverrides says, so each of the two doubly delivered skills keeps one copy per client (CPA-V-07) - #1074

Merged
seathatflowsinourveins merged 6 commits into
mainfrom
claude/cc-native-practice-plugin-skill-identity-20261013
Oct 13, 2026
Merged

seathatflowsinourveins merged 6 commits into
mainfrom
claude/cc-native-practice-plugin-skill-identity-20261013

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Scope

  • What this PR changes, in one or two sentences: CPA-V-07 of the Claude-family practice audit (claude-practice-audit-converged-20261012, P2): typesafe-ai and claude-api are each delivered to Claude Code by two routes and listed twice with one description, and skillOverrides cannot hide a plugin's skill (measured on 2.1.296 and documented), which the practice page did not say. The manifest rows name their twin, scripts/skills_status.py reports duplicate_listing where the plugin is present and the other copy is not hidden, the usage tracker counts both listings (the most recent use), a globally disabled bundled twin is one copy, the practice page states the exception, and the claude-api currency comparison is recorded. The host's own skillOverrides entries are the command center's act (the diff is in the decision record and below).
  • Base commit: 24a209016178de9850f9b36d351dc72f5b1b0a26 (main after fix(trading): share strict paper URL normalization across credential gates #1080; rebased for landing from 993cc890b3ff3fc95fa7cdba51edd726565f9ace, main after Keep lane-A journal continuity for session-only config changes #1068, the head when this was rebased for opening).
  • Lane: lane:foundation
  • Owned paths touched: adoption/skills/manifest.json, scripts/skills_status.py, tools/skill-usage/skill_usage.py, .claude/skills/claude-native-practice/reference/ (instructions-skills.md, slots.json), docs/decisions/ (one new record), evidence/artifacts/plugin-skill-identity-20261013/, catalogs/foundation/ (two line citations of the manifest that the added lines shifted), evidence/artifacts/claude-native-practice-20261009/claude-native-slot-manifest.json (the snapshot label's hash of slots.json), tests/test_skills_manifest.py, tests/test_skills_status.py, tests/test_skill_usage_daily.py, manifests/evidence.json.
  • Commits: five: the change (2dae97511), round 1 after the reads of that head (48e083e88, below, a fast-forward of it), the advisory delta for the reads that round 1 removes (f7e0cd703, below), round 2 (7c7d667f1, below, a fast-forward of f7e0cd703) and round 3, the final bounded commit of the local review of the rebased head (below). On 24a209016 (the rebase below) they are 3ec111b26, e3469d5dc, 013459679, 5a6635370 and f92f1eeec; the reads and rounds above name the earlier heads.

The change

Part Change
Manifest rows typesafe-ai carries claude_plugin: typesafe@typesafe-ai and claude_skill_name: typesafe:typesafe-ai; claude-api keeps native_plugin and claude_skill_name: claude-api:claude-api and gains bundled_twin: claude-api (the bare name the client lists its own copy under). load_manifest checks the fields (a plugin id is <plugin>@<marketplace>; claude_plugin and native_plugin exclude each other; bundled_twin belongs on a native_plugin row; claude_skill_name is <plugin>:<skill>)
Status check where the plugin is present (a user-scope install in the native installed_plugins.json and enabledPlugins true in settings.json) the other copy must be hidden: a claude_twin entry, ok or duplicate_listing (also for a skillOverrides of the wrong type); the personal copy's expected listing on such a host is off. A host without the plugin is unchanged. Ids and states only
Usage tracker claude_row adds the uses and the context of the two listings of a skill and takes the last use of the listing that was used; a zero left by the hidden twin is not read as no use
Practice page the skills-library row (instructions-skills.md, slots.json) states the plugin exception, what was measured and where the manifest and the check carry it
Citations and label the rows added to the manifest shift the lines that catalogs/foundation/upstream-surface-dispositions.json (:920 to :923, twice) and catalogs/foundation/new-wsl-architecture-20261001.json (:75 to :76, :100 to :103) cite, and the practice page's notes change the hash of slots.json that the snapshot label of claude-native-slot-manifest.json records (the field and the note's list of slots whose notes differ); each had a test that failed in the first sweep (test_upstream_surface_watch, test_architecture_pin_source, test_native_practice_snapshot_provenance)
Not changed the profile template (typesafe-ai: on, no claude-api entry: a fresh host installs one copy), the Codex route, the installers

SOTA sources

  • code.claude.com/docs/en/skills.md L909 ("Plugin skills are not affected by skillOverrides. Manage those through /plugin instead."), L928 and L333, fetched 2026-10-13T10:29:20Z (107,130 bytes, sha256 35fd2c11f870fbe1497a53796de31b2a6b760645e724bda58894525356d4602a; receipts/docs-skills-md-excerpt.txt).
  • anthropics/claude-code@3deb8736 CHANGELOG.md L5319: "skillOverrides setting now works: off hides from model and /, user-invocable-only hides from model only, name-only collapses description".
  • The installed Claude Code 2.1.296: the init record's skill list under --settings overlays (receipts/listing-init-records.json) and the folder it unpacks for the bundled claude-api skill (receipts/claude-api-currency.txt).
  • typesafe-ai/skills@65a39f393687 README.md: the Claude Code plugin route (claude plugin install typesafe@typesafe-ai) and the skills.sh route for other agents.
  • anthropics/skills@dbd4588f9e1033efb41dad4bef2f7947c8993d44 .claude-plugin/marketplace.json L46: the claude-api plugin entry the manifest row pins; its claude-api folder is the plugin side of the currency comparison.
  • This repository: docs/decisions/2026-09-30-skills-llm-native-listing.md (the listing budget), docs/decisions/2026-10-10-daily-skill-invoke-rate.md (the usage tracker's claude_skill_name), the audit's CPA-V-07 text.

Evidence-class table

Claim Evidence class Command / receipt
The client lists a plugin's skill as <plugin>:<skill>; a skillOverrides entry hides only the copy it names (the personal or the bundled one), a qualified key changes nothing, and both bare names off lists one copy of each (77, 76, 76, 77, 77, 75 skills) native_proven, source_review receipts/listing-init-records.json (six headless init records, five of them captured twice with identical counts and names); the documentation lines and the changelog line cited above
An agent that preloads the skill by its short or its qualified name still gets the body with the personal copy hidden native_proven receipts/preload-probes.json (10 of 10 probes quote the body's first heading, among them 3 short-name and 3 qualified-name probes with the personal copy hidden)
The plugin's claude-api copy at the pin lacks nothing of substance that the client's bundled copy has: 91 of the bundled folder's 94 files are byte for byte the plugin's, the other three differ in early-access wording local_integration receipts/claude-api-currency.txt (file hashes and difflib summaries)
The first host's status fails today for exactly these two rows and passes with the two proposed skillOverrides entries local_integration receipts/host-overlay-simulation.json (the host's real settings read, the entries substituted in memory, nothing written)
The loader, the twin check, its states and the combined usage row behave as stated local_integration PluginTwinTests, TwinListingUsageTests, test_skills_manifest.py; 22 of the 142 tests of the three modules fail on the sources of the base (receipts/fail-before.txt), and 41 of 41 mutants are killed (receipts/mutants-v07-41.txt)
The practice page and its JSON state the plugin exception local_integration PluginTwinTests.test_the_practice_page_and_its_json_state_the_plugin_exception (the audit's fail-first page test)

Local commands run

$ python3 -m unittest tests.test_skills_manifest tests.test_skills_status tests.test_skill_usage_daily  ->  Ran 142 tests, OK (38 + 88 + 16)
$ the same three files against the sources of 3f1a921d  ->  22 tests fail or raise (3 + 11 + 8; receipts/fail-before.txt)
$ python3 -E -s -B <mutation script over scripts/skills_status.py and tools/skill-usage/skill_usage.py, in a scratch copy, idle-class>  ->  41 of 41 mutants killed
$ python3 scripts/skills_status.py  ->  on the first host today: fail  claude-api ... twin=duplicate_listing; fail  typesafe-ai ... listing=mismatch(on) twin=duplicate_listing (the two rows of this change; the other rows pass)
$ <the status with skillOverrides.typesafe-ai and skillOverrides.claude-api substituted by "off" in memory>  ->  result ok
$ python3 -m unittest <the modules that name a changed file, one process each, idle-class>  ->  168 modules, one process each, idle-class: the last run of every module exit 0 except test_token_e2e_grader (1 of 624 tests: git clone --local across devices into the scratch tmp, an environment failure that names no changed file; the first two sweeps' failures in test_architecture_pin_source, test_native_practice_snapshot_provenance, test_upstream_surface_watch and test_new_wsl_handbook were this change's derived files, fixed and rerun green); at the rebased head the 12 modules that name a changed file or a derived family reran exit 0 (test_skills_manifest 38, test_skills_status 88, test_skill_usage_daily 16, test_skill_usage 221, test_architecture_pin_source 4, test_native_practice_snapshot_provenance 9, test_upstream_surface_watch 116, test_new_wsl_handbook 88, test_evidence_manifest 5, test_runtime_worker_openhands_push_gate 127, test_adoption_docs_consistency 39, test_validate 86)
$ python3 scripts/validate.py  ->  exit 0 (11,893 files hashed); python3 scripts/evidence_manifest.py --check  ->  passed; git merge-tree --write-tree origin/main HEAD  ->  clean against main 993cc890; the push-gate advisory derivation adds no read for the changed scripts (the 4 reads it lists belong to #1057, which landed with its own delta; the module passes at the head); the privacy scan of the added lines: 0 account, host, home-path, e-mail or token hits

Review round 1 (one fast-forward commit on 2dae97511): Astra's read (two P1, one P2) and the local codex review (three P2)

Astra's read of 2dae97511 is CHANGES_REQUESTED (verdict file full1-pr1074-2dae97511/VERDICT.md, sha256 50828557...) and the local codex review said "patch is incorrect" (three P2). The command center asked for one commit for the six; each was reproduced on 2dae97511 and has a test that fails there. 2dae97511 merges clean against main ba6afd8d7 (exit status read directly): a plain fast-forward.

Finding Fix Test (fails before the fix)
P1 (Astra), receipts/claude-api-currency.txt:45: the digest of the prompt that the client expands for its bundled skill, whose first line names the folder the client unpacked the skill into on this host, resolved to no published file and its preimage holds a host path (a confirmation oracle: the #161 class) the digest and every prefix of it are removed; the plugin file's digest (an upstream blob at the pin) stays; the receipt says why; no history rewrite tests/test_plugin_skill_identity_receipts.py (every digest of the evidence folder, its README and the record resolves to a registered file, a manifest pin or a public page with its URL and fetch time; the text receipts are in its reach); the mutant that writes the digest back is killed
P1 (Astra), scripts/skills_status.py:495-499: a host that disables bundled skills as a whole has one copy of claude-api and was reported as a duplicate the bundled twin is hidden by disableBundledSkills true or CLAUDE_CODE_DISABLE_BUNDLED_SKILLS; measured on 2.1.296, 30 init-record runs (receipts/global-disable-init-records.json): 77 skills listed, 61 with either, the bundled copy gone and the plugin's kept; only the boolean true counts in the setting; the variable reads 1, true, TRUE, yes, on and a padded 1 (not 0, 2, false, off, no, y, t, enabled or empty); the settings env entry overrides the process environment in both directions; documented in code.claude.com/docs/en/env-vars (receipts/docs-env-vars-excerpt.txt) and anthropics/claude-code@3deb873687379f0c199a69fdaccc65f0865f7a29 CHANGELOG.md L4500. A personal twin stays a duplicate test_a_bundled_twin_is_hidden_by_disabling_bundled_skills_as_a_whole, ..._by_the_environment_equivalent_of_that_setting, test_the_env_entry_of_settings_overrides_the_environment_and_may_be_a_number_or_a_boolean, test_disabling_bundled_skills_hides_no_personal_copy_and_an_unreadable_settings_file_disables_nothing
P2 (Astra), tools/skill-usage/skill_usage.py:2542-2544: the combined row took the first used listing's date, so a plugin used 7 days ago and a personal copy used an hour ago reported 7 days ago the used listings' cells are put in order (never, today, N days, and the older table's N hours ago) and the most recent is taken; a cell that cannot be ordered makes the date unknown test_the_combined_last_use_is_the_most_recent_of_the_listings_that_were_used_whichever_is_listed_first (both twin kinds, both orders, nine spellings), test_a_listing_that_was_not_used_does_not_count_and_one_that_cannot_be_put_in_order_makes_the_date_unknown
P2 (local review), scripts/skills_status.py:471-474: a registry row with scope: user was enough, even at another commit or with its cached skill gone the plugin that stays must be the pinned one (plugin_status: pin, folder, bytes); an installed and enabled plugin that is not is plugin_invalid, hidden twin or not test_a_replacement_plugin_that_is_not_the_pinned_one_is_not_accepted_for_a_hidden_copy (four breakages, hidden and not), test_a_native_plugin_row_that_is_not_as_pinned_reports_its_twin_as_invalid_too
P2 (local review), :710-711: the registry was read from the skills CLI's trimmed link folder while the settings are read as the client reads them, so a padded or blank CLAUDE_CONFIG_DIR sent them to different folders the registry is read from the folder of the settings (claude_config_dir: the client's configuration folder, which CLAUDE_CONFIG_DIR moves with "all settings, session history, and plugins"); the native plugin status reads it too test_the_plugin_registry_is_read_from_the_folder_of_the_settings_that_enable_the_plugin, test_the_native_plugin_status_reads_the_registry_of_the_folder_the_client_uses
P2 (local review), :759-760: a Claude-only copied row with claude_plugin took the copied-skill branch, which never computed the twin the branch computes the twin and the adjusted listing test_a_claude_only_copied_skill_gets_the_twin_policy_too

The cases were reproduced on 2dae97511 and pass on this head (receipts/fail-before-round1.txt: 14 tests fail or raise there); the original fail-first tests replayed at the exact base 993cc890b give the same 22 of 142 (receipts/fail-before-at-main-993cc890b.txt, Astra's P3 6, which also relabels receipts/fail-before.txt); 30 of 30 mutants of the round's changes are killed (receipts/mutants-round1-30.txt). The new helper sits beside claude_row, so no line number cited by another decision record moves (the first version put it near the top: test_skill_usage caught the shift of ledger_path_issue, and #1043's round 8 moves the same file by five lines).

Checks at 48e083e88 (idle-class, read at this head): python3 scripts/validate.py exit 0 (11,899 files); tests.test_skills_manifest 38, tests.test_skills_status 98, tests.test_skill_usage_daily 18, tests.test_plugin_skill_identity_receipts 3 OK; the 16 modules that name a changed file or a derived family (test_skill_usage 221, test_upstream_surface_watch 116, test_adoption_docs_consistency 39, test_new_wsl_handbook 88, test_validate 86, test_published_digest_privacy 15, test_architecture_pin_source, test_native_practice_snapshot_provenance, test_rtk_receipt_locators, test_skill_correction_claims, test_evidence_manifest, test_skill_routing) rerun exit 0 at the final tree after a 74-module sweep whose only failures were test_skill_usage (the locator shift above, fixed) and test_token_e2e_grader (a test clone hard-links across devices: the same failure at every earlier head); git merge-tree --write-tree origin/main HEAD clean against main ba6afd8d7 (exit status read directly); the digest audit of the added lines: 19 occurrences, 15 registered files, 1 manifest pin, 3 public-page digests, 0 unresolved; gitleaks on the commit: no leaks; the privacy scan of the added lines: 0 account, host, home-path, e-mail, non-ASCII or token hits.

The advisory delta of round 1 (f7e0cd703)

Round 1 moved the registry read of the native plugin checker into plugin_status and removed the twin check's own registry reader, so the OpenHands push gate's derivation counts two reads fewer in scripts/skills_status.py (346 against the 348 that main's newest delta expects); no read is added. tests.test_runtime_worker_openhands_push_gate derives its reads from the committed tree and I had run it on the working tree: it passed there and failed on the committed 48e083e88, so the delta is its own commit: blueprints/runtime-workers/openhands/evidence/unclassified-gate-reads-20261013-pr1074-delta.json and its index entry (removed_by_script: scripts/skills_status.py 2; the four tools/credentials/set_credential.py reads are #1057's, restated because the newest delta is cumulative). At f7e0cd703: python3 scripts/validate.py exit 0 (11,900 files), tests.test_runtime_worker_openhands_push_gate 127 OK on the committed tree, gitleaks no leaks. The statement above that the push-gate inventory is unchanged held for the first commit only. Another pull request that appends a delta to the same index (#1062 does) is re-derived by whichever lands second.

Review round 2 (one fast-forward commit on f7e0cd703): the local codex review (one P2) and Astra's read of 48e083e8 (one P2 blocker, answered; one adjacent P2)

The local codex review of f7e0cd70 said "patch is incorrect" (one P2) and Astra's read of 48e083e8 (verdict file fix1-pr1074-48e083e88/VERDICT.md, sha256 32f16c49...) is CHANGES_REQUESTED on one P2 that f7e0cd703 had already answered and one adjacent P2. The co-op asked for one commit for the two visibility cases; each was reproduced on f7e0cd703 and has a test that fails there.

Finding Fix Test (fails before the fix)
P2 (local review), scripts/skills_status.py:526-527: with CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL=1 and the pinned plugin enabled the client lists only the plugin's copy, and the check saw only skillOverrides and the all-bundled switch, so it reported duplicate_listing for a valid single-copy host the bundled twin is also hidden by the client's switch for that skill (BUNDLED_SKILL_SWITCHES, keyed by the bundled name). Source: the installed 2.1.296 binary, the function that initializes the bundled skills (registerClaudeApiSkill is called with disabled: env.CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL === true, byte offset 225255110); the documentation and the changelog at the pin name neither the variable nor the function. Measured in 14 init-record runs (receipts/skill-switch-init-records.json): 77 skills listed, 76 with the switch, the bundled copy gone and the plugin's kept; the spellings are the all-bundled switch's; the settings env entry overrides the process environment in both directions test_the_skill_specific_switch_hides_the_bundled_twin_and_only_that_twin (ten spellings, five settings-env cases, a twin of another name, a personal twin)
P2 (Astra, follow-up 2: adjacent, inherited), :525, :735, :767: a disabled native plugin with the bundled twin hidden passed although no route lists the skill with the plugin not listed (not enabled, or not registered) and the bundled twin hidden by skillOverrides, the all-bundled switch or the skill's own, the twin entry is not_listed and the row fails; a disabled plugin with the bundled copy visible is one copy and passes; a personal twin keeps the manifest's own listing check test_a_bundled_twin_that_no_route_lists_is_not_listed_and_fails_the_row (four ways to hide, an unregistered plugin, and the controls)
P2 (Astra, blocker 1): the push gate's read inventory had not been refreshed after the plugin checker was refactored answered by f7e0cd703 (the delta unclassified-gate-reads-20261013-pr1074-delta.json, 346 reads, removed_by_script scripts/skills_status.py 2, and its index entry; the module passes on the committed tree) before this round was asked for; this round changes no count: tests.test_runtime_worker_openhands_push_gate 127 OK on the committed 7c7d667f1, and the body's earlier claim that it passed at 48e083e88 was wrong and is corrected in the section above tests.test_runtime_worker_openhands_push_gate on the committed head
P3 (Astra, 3 and 6) the receipts README row of the older fail-before receipt names the base it was taken against and points to the exact-base replay; the record says what is reported (never an install path or a credential) and what is read (the plugin's SKILL.md at the registry's install path, inside the plugin cache) (the text)

receipts/fail-before-round2.txt (2 tests fail on f7e0cd703, 16 failing subtests), receipts/mutants-round2.txt (14 of 14 killed). Declined with the reason: Astra's P3 5 (a regression for abbreviated digests): a fixture naming a prefix of the removed digest would publish that prefix, which the standing rule forbids; the receipt test holds every 64-hex value and the removed digest's absence is checked by the reviewer's scan.

Checks at 7c7d667f1 (idle-class, read at this head): python3 scripts/validate.py exit 0 (11,903 files); tests.test_skills_manifest 38, tests.test_skills_status 100, tests.test_skill_usage_daily 18, tests.test_plugin_skill_identity_receipts 3 OK; tests.test_runtime_worker_openhands_push_gate 127 OK; the digest audit of the added lines: 7 occurrences, 7 registered files, 0 unresolved; gitleaks on the commit: no leaks; the privacy scan of the added lines: 0 hits. Main moved after the round was written (df3a477c8 #1070 and 77cc7ef42 #1075): this branch now conflicts with it on manifests/evidence.json and the delta index (#1070 appended a delta), so the landing rebase re-derives the cumulative delta, as #1057's landing required of #1062.

Landing rebase onto 24a209016 and review round 3 (the local review of the rebased head, two P2; one final bounded commit, pushed with the rebase)

The pull request went DIRTY on manifests/evidence.json and the advisory delta index as main moved (#1070 appended a delta of its own for scripts/validate.py, #1075, #1080). It was rebased once with the owning tools and no hand edit of a derived file: each commit is cherry-picked onto main; a conflict in the registry or the index takes main's file and the pull request's part is re-derived (host_receipts.register_file for the rows the commit changed; the delta of the one commit that carried one is re-derived with the native GitTree and derive_ci_protected over the tree that now holds #1057's and #1070's reads). The local review of the rebased head then found two P2, and the command center ruled one final bounded commit, pushed together with the rebase as one push; any further P2 or P3 of the next reads goes to its adjudication as a follow-up, with no further commit.

  • Per file (git patch-id --verbatim of the -U0 --binary diff against each side's own merge base: old head 7c7d667f1 against 993cc890b, rebased head 5a6635370 against 24a209016): 35 files before and after, none only on one side; 33 have the same patch; the 2 that differ are the ones that had to: manifests/evidence.json and the advisory delta (re-derived). The delta index has the same patch (the entry appended after main's). Per commit, with the registry, the index and the delta left out: 4 of 4 have the same patch-id.
  • Registry rows: 34 PR-owned rows at the old head, 0 missing at the rebased head, 0 not current (each row's sha256 and byte count are those of the file there); only the delta's row and the index's differ from the old head's; the other 11,873 rows are main's, unchanged; no row is added beyond main's and the pull request's.
  • The advisory delta was re-derived over main's tree: 347 reads against the 344 of the baseline snapshot (tools/credentials/set_credential.py 4 and scripts/validate.py 1, with the scopes those deltas gave them; scripts/skills_status.py 2 removed); source_commit is null as before (derived from the local commit that precedes the delta; the published head holds the same scripts).
Finding Fix Test (fails before the fix)
P2-1, scripts/skills_status.py:482-484: env_text took a number only as a Python int, so a JSON 1.0 or 1e0 (a float) was no text and the switch was reported unset, where the client sets it env_text is JavaScript's String() of the JSON value, as the client reads an env entry. Measured on 2.1.296 in 28 init-record runs (receipts/numeric-env-init-records.json): 61 skills listed, the bundled claude-api gone, with 1, 1.0, 1e0, 10e-1, 0.1e1, 1E0, 1.00, true, "1", [1], ["1"], [true] and [[1]]; 77 with 0, 0.0, 2, 1.5, -0, null, [], an object, [1, 2], ["1", "x"], [null], [null, 1] and [1, true]; the skill's own switch the same for 1.0 and 1e0 test_a_number_or_an_array_in_an_env_entry_is_the_text_javascript_makes_of_it (27 subtests of raw settings text through the reader and the row's twin state, three for the skill's own switch, a deep array; 13 subtests fail on 7c7d667f1)
P2-2, scripts/skills_status.py:514-516: read_switches returned None for a settings file that does not exist, which the callers read as "disables nothing", so the process environment's switch was ignored and a host with no settings file and the switch set was reported with its bundled copy listed, where the client lists neither copy a file that does not exist is empty settings, so the process environment's switches count; with no settings no plugin is enabled either, so the row is not_listed and fails; a file that exists and cannot be read still disables nothing test_a_missing_settings_file_is_empty_settings_and_the_environment_switch_still_counts

The cases were reproduced on 7c7d667f1 and pass on this head (receipts/fail-before-round3.txt); 12 of 12 mutants of the commit are killed (receipts/mutants-round3-12.txt; the first run left an array read as its first element only, and the test now holds the measured multi-element spellings). The decision record has a "Review round 3" section.

Checks at f92f1eeec (idle-class, read at that head): python3 scripts/validate.py --registration-base origin/main --registration-before 7c7d667f1 exit 0 (11,910 files hashed); the 62 modules that name a changed file of the commit or gate the registry, one process each: 3,745 tests, 61 exit 0 (tests.test_skills_status 102, tests.test_skills_manifest 38, tests.test_skill_usage_daily 18, tests.test_skill_usage 221, tests.test_plugin_skill_identity_receipts 3, tests.test_adoption_status 141, tests.test_adoption_docs_consistency 39, tests.test_validate 86, tests.test_registry_rows 25 and tests.test_runtime_worker_openhands_push_gate 127 among them); the one other, tests.test_workflow_hardening, fails in a nested fixture test of the secret scanner (2nd pipe should have enough spare: the kernel's pipe-page limit under this host's load) and passes on a rerun of that test and on a clean origin/main checkout, and the workflow files are not changed by this pull request; the local premerge check (a throw-away merge of the head onto main 04f64a056): git merge-tree --write-tree exit status 0, the push-gate, skills-status and registry-rows modules 254 OK under GITHUB_ACTIONS=true, scripts/validate.py exit 0; gitleaks on the five commits: no leaks; identities all the no-reply one; the privacy scan of the added lines: 0 account, host, home-path, e-mail, non-ASCII or token hits (47 digests before the rebase and 50 now: the three registry rows of the new receipts).

Registry-only rebase onto 329985de1 (the command center's ruling on the re-review; no content change)

Main moved again (#1062 landed, with the two pull requests before it) and the pull request went DIRTY on manifests/evidence.json and the advisory delta index. It was rebased once, pushed with a lease on f92f1eeec, by the same procedure and tools as the rebase above (each commit cherry-picked onto main; a conflict in the registry or the index takes main's file and the pull request's part is re-derived: host_receipts.register_file for the rows the commit changed, the delta re-derived with the native GitTree and derive_ci_protected over the tree that now holds #1062's reads). The merge-tree check before the rebase conflicted in those two files and no other.

  • Per file (git patch-id --verbatim of the -U0 --binary diff against each side's own merge base: old head f92f1eeec against 24a209016, new head c17f820d9 against 329985de1): 38 files before and after, none only on one side; 36 have the same patch; the 2 that differ are the ones that had to: manifests/evidence.json and the advisory delta (re-derived). The delta index has the same patch (the entry appended after main's). Per commit, with the registry, the index and the delta left out: 5 of 5 have the same patch-id.
  • Registry rows: 37 PR-owned rows at the old head, 0 missing at the new head, 0 not current (each row's sha256 and byte count are those of the file there); 3 differ from the old head's row: the delta's and the index's, and catalogs/foundation/upstream-surface-dispositions.json, whose file main changed too (Detect drift between the installed Claude agent definitions and the repository's (CPA-V-10) #1062, one line; the two changes merged without a conflict and the row is the merged file's); the other 11,905 rows are main's, unchanged; no row is added beyond main's and the pull request's.
  • The advisory delta was re-derived over main's tree: 351 reads (main's 353 less the 2 reads of scripts/skills_status.py that this change removes); source_commit is null as before (derived from the local commit that precedes the delta; the published head holds the same scripts).
  • Checks at c17f820d9 (idle-class): python3 scripts/validate.py --registration-base origin/main --registration-before f92f1eeec exit 0 (11,942 files hashed); python3 scripts/evidence_manifest.py --write changes nothing; git merge-tree --write-tree origin/main HEAD exit 0; tests.test_runtime_worker_openhands_push_gate 127, tests.test_registry_rows 25, tests.test_skills_status 102, tests.test_skills_manifest 38 and tests.test_validate 86 tests, each OK; the privacy scan of the added lines (2,563): 0 account, host, home-path, e-mail, non-ASCII or token hits.

Decision record

docs/decisions/2026-10-13-plugin-skill-identity.md: the context, the measurement, the claude-api currency comparison, the decision (six parts), the alternatives, what is not covered and the conditions that overturn it.

Host evidence

Only relevant when this PR adds or changes files under evidence/hosts/. This PR changes none.

Checklist

  • New/changed GitHub Actions are pinned to a full commit SHA with a version comment (no floating tags). None changed.
  • New/changed workflows declare top-level permissions: {} and grant each job only what it needs. None changed.
  • No secrets are printed, logged or committed; no new required secret was added without a documented owner. The status check reads plugin ids and states, never an install path or a credential, and its report holds ids and states only.
  • No new paid hosting, subscription or billing surface was introduced. The listing runs were killed before any model call; the ten preload probes cost 0.075 USD in all.
  • Peer-owned untracked files and worktrees were preserved (not deleted, moved or overwritten).

Host step for the command center (nothing is applied by this PR)

  1. In the first host's ~/.claude/settings.json, skillOverrides: set "typesafe-ai": "off" (it is "on") and add "claude-api": "off". The simulated result is ok for the whole status; python3 scripts/skills_status.py shows twin=ok for both rows afterwards, and the client lists one copy of each (75 skills in the measured run).
  2. Re-applying the profile template with tools/adoption/apply_claude_settings.py sets typesafe-ai back to on (the template wins on shared keys), and the status then reports the duplicate again; repeat step 1, or see the follow-up.

Follow-ups

  • From the re-review of the rebased head (P2; past the command center's bound, ruled a follow-up): scripts/skills_status.py:518, env_switch strips the value with Python's str.strip(), where the client trims with JavaScript's trim(); the script already holds that set (JS_TRIM_CHARS, :130, whose copy the tests keep equal to the installer's). Measured at this head with the script's own function: a leading U+FEFF ("\ufeff1") reads as unset, though trim() removes it, and a trailing U+0085 ("1\^E") reads as set, though trim() keeps it. Not measured on the client for these two characters (the earlier padding measurements used ASCII spaces). On the next touch: strip with JS_TRIM_CHARS, with regressions both ways ("\ufeff1" sets the switch, "1\^E" does not).
  • From the Claude read at c17f820d (a read-only evidence reviewer; the client facts come from the installed 2.1.296 binary, read without running it and cited by byte offset; adjudicated by the command center as Follow-ups, verdict PASS at c17f820d; record pr1074-claude-read-and-adjudication-f92f1eee.md, sha256 b1e923d153080faec0cbaf6bcf7890a802ea82ee9e4b69e46cf3498024812a10):
    • P2-1, a false ok in a non-default setup: scripts/skills_status.py reads plugins/installed_plugins.json under the config directory (claude_config_dir, :290-294; the reads at :775 and :784), where the 2.1.296 client resolves its plugin folder from CLAUDE_CODE_PLUGIN_CACHE_DIR (and a cowork variant and seed directories; the reader's offsets 211876595, about 219447753 and 211876705; the variable's name stands in the binary at offsets including 211876536). With that variable set and the plugin installed elsewhere, a doubly listed skill reports ok. None of these variables is set on this host's sessions, and the overall result still fails in that setup, but for the wrong reason. Follow-up: resolve the plugin folder exactly as the client does, or report an unsupported-root state, with a fail-first test.
    • P3 1, settings precedence: the checker reads user settings only; the client merges user, project, local, flag and policy settings and --add-dir folders; the record's not-covered list names installs only. Follow-up: document it or implement it.
    • P3 2, an unreadable settings file can leave the claude-api row passing. Follow-up: an explicit state for it.
    • P3 3, citations: some client-behaviour claims cite documentation only; cite the binary offsets instead (for example the enabled rule at about 219684603).
  • From the local review of the rebased head: the two P2 above are answered by the final bounded commit; the command center's bound stands (any P2 or P3 of the next local review or Astra's delta goes to its adjudication as a follow-up, with no further commit). The same class stands in another script that this change does not own: scripts/adoption_status.py settings_env_text (:441-444) reads an env entry only as a string or an integer, so a JSON 1.0 for the depth variable reads as unset there where the client reads it as 1; it is not changed here (a separate reader, owned by Detect drift between the installed Claude agent definitions and the repository's (CPA-V-10) #1062; trigger: its next change, with the same measured reading as receipts/numeric-env-init-records.json).
  • Moving the decision into the template (a template off plus the plugin's installation in the bootstrap, so that a re-application keeps one copy) needs a fresh-host design: the template cannot install the plugin, and off without the plugin would leave Claude Code without the typesafe skill. Trigger: the first time the applier resets the entry on a host that has the overlay.
  • Plugin installs at project or managed scope are not counted as present (user scope only, the scope the vendor's README and the installer use). Trigger: a host that installs either plugin at another scope.
  • Repeat receipts/claude-api-currency.txt at each re-pin of the claude-api row and when a client release changes the bundled skill (the overturn condition).
  • Hostname text already stands in manifests/evidence.json (18 lines) and in the new-WSL handbook (two lines); none is added here (Astra's P3 8). A hygiene pull request owns it.
  • From Astra's read of 2dae97511: (P2, adjacent; corrected in round 2: a disabled plugin with the bundled twin hidden is not_listed) the plugin row's own pass for a disabled plugin whose bundled copy is visible stays: one copy, the client's, and canonical is about the plugin's files, not its enablement; (P3) project and local scope installs and the profile replay (apply_claude_settings.py restores typesafe-ai: on) stay the recorded boundaries; (P3, inherited) catalogs/sota-convergence/manifest-20260926.json:6521 anchors a mutation-testing deferral at adoption/skills/manifest.json#L788-L791, already stale at the base (trigger: that catalog's next edit); (P3) the full skills-page capture, the pin JSONs of the cited upstream sources and the raw init streams and probe transcripts are not in the packet (the excerpt receipts carry the fetched page's size and sha256, the pins are readable at the local mirrors).
  • From Astra's read of 48e083e8: (P3 4) the project-scope and profile-replay boundaries stay as recorded, with their triggers; (P3 7) the inherited catalog anchor and the hostname lines; (P3 8) the full skills-page and environment-page captures, the raw init streams and the capture and mutation drivers are not in the packet (the excerpt receipts carry the fetched pages' sizes and sha256; the drivers stay in the author's record).

🤖 Generated with Claude Code

…whatever skillOverrides says, so each of the two doubly delivered skills keeps one copy per client (CPA-V-07)

typesafe-ai and claude-api are each delivered to Claude Code by two routes (this repository's skills.sh copy and the vendor's plugin; the client's bundled copy and the pinned plugin), so each is listed twice with one description
(660 + 1,068 characters in every session), and the practice page said that skillOverrides governs skills. Measured on Claude Code 2.1.296 in headless init records: an entry hides only the copy it names (the personal or the
bundled one), the qualified names typesafe:typesafe-ai and claude-api:claude-api stay listed and a qualified key changes nothing; the documentation says so (skills.md L909, L928). Ten preload probes show that an agent that
preloads the skill by its short or its qualified name still gets the body with the personal copy hidden.

- adoption/skills/manifest.json: typesafe-ai names its plugin (claude_plugin, claude_skill_name typesafe:typesafe-ai); claude-api names the bare name the client lists its own copy under (bundled_twin). load_manifest checks the
  fields.
- scripts/skills_status.py: where the plugin is present (a user-scope install in installed_plugins.json and enabledPlugins true) the other copy must be hidden; the row gets a claude_twin entry, duplicate_listing when it is not,
  and the personal copy's expected listing there is off. A host without the plugin is unchanged.
- tools/skill-usage/skill_usage.py: the usage tracker adds the uses and the context of the two listings of a skill.
- The practice page (instructions-skills.md, slots.json) states the plugin exception.
- Derived files that follow: the line citations of two catalogs that the added manifest lines shifted (upstream-surface-dispositions, new-wsl-architecture), the generated new-WSL handbook and its receipt's digests (regenerated
  with scripts/build_new_wsl_handbook.py --write), and the practice snapshot label's hash of slots.json; their tests failed in the first sweeps and pass.
- The claude-api currency comparison is recorded: 91 of the 94 files of the bundled folder are byte for byte the plugin's, the other three differ in early-access wording, so the plugin stays and the bundled copy is the one
  hidden. The host step (skillOverrides typesafe-ai off, claude-api off) is the command center's act and is simulated against the first host's real settings: fail before, ok after. The profile template is unchanged.

Tests: 22 of the 142 tests of the three modules fail on the sources of the base, among them the audit's two fail-first tests; 41 of 41 mutants killed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ne copy, the replacement plugin must be the pinned one, the registry is read from the client's folder, and the last use is the most recent

Astra's read of 2dae975 (CHANGES_REQUESTED: two P1 and a P2) and the local codex review (three P2) were reproduced on that head; each of the six has a test that fails there (receipts/fail-before-round1.txt).

- evidence/.../receipts/claude-api-currency.txt (P1, Astra): the digest of the prompt that the client expands for its bundled skill is removed, with every prefix of it: its first line names the folder the client
  unpacked the skill into on this host, and no published file hashes to it. The plugin file's digest (an upstream blob at the pin) stays. tests/test_plugin_skill_identity_receipts.py holds that every digest in the
  evidence, its README and the record resolves to a registered file, a manifest pin or a public page quoted with its URL and fetch time.
- scripts/skills_status.py (P1, Astra): a bundled twin is hidden by disabling bundled skills as a whole, disableBundledSkills true or CLAUDE_CODE_DISABLE_BUNDLED_SKILLS. Measured on 2.1.296, 30 init-record runs
  (receipts/global-disable-init-records.json): 77 skills listed, 61 with either, the bundled claude-api gone and the plugin's kept; the spellings and the precedence of the setting, the variable and the settings env
  entry are the tests'. A personal twin is not a bundled skill and stays a duplicate.
- scripts/skills_status.py (P2, local review): the plugin that stays must be the pinned one (pin, folder, bytes: plugin_status, the native plugin checker's own read); an installed and enabled plugin that is not is
  plugin_invalid whether or not the other copy is hidden.
- scripts/skills_status.py (P2, local review): the registry is read from the folder of the settings that enable the plugin (the client's configuration folder), not from the skills CLI's trimmed link folder; the native
  plugin status reads it too.
- scripts/skills_status.py (P2, local review): a Claude-only copied row with claude_plugin gets the twin check and the adjusted listing, as a linked one does.
- tools/skill-usage/skill_usage.py (P2, Astra): the combined last use is the most recent of the used listings, the cells put in order (never, today, N days, N hours ago); an unorderable cell makes it unknown.
- The record has a review round 1, the receipts README five rows, the older fail-before receipt names the base it was taken against and its replay at the exact base 993cc89 gives the same 22 of 142; 30 of 30
  mutants killed. The new helper sits beside claude_row so that no cited line number of the file moves.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… round 1 removes from scripts/skills_status.py

Round 1 moved the registry read of the native plugin checker into plugin_status and removed the twin check's own registry reader, so the OpenHands push gate's derivation counts two reads fewer in
scripts/skills_status.py (346 against the 348 that main's newest delta expects). tests.test_runtime_worker_openhands_push_gate derives its reads from the committed tree; round 1 was first checked on the
working tree, so the continuous integration found the count first.

- blueprints/runtime-workers/openhands/evidence: unclassified-gate-reads-20261013-pr1074-delta.json and its index entry (removed_by_script: scripts/skills_status.py 2; the four set_credential.py reads are
  #1057's, restated because the newest delta is cumulative from its baseline), derived with the native GitTree and derive_ci_protected at the committed head.
- the record and the receipts README name the delta. Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…laude-api hides the bundled twin, and a bundled twin that no route lists is not_listed

The local codex review of f7e0cd7 said "patch is incorrect" (one P2) and Astra's read of 48e083e is CHANGES_REQUESTED (one P2 blocker, answered by f7e0cd7, and an adjacent P2); the co-op asked for one commit
for the two visibility cases. Each was reproduced on f7e0cd7 and has a test that fails there (receipts/fail-before-round2.txt).

- scripts/skills_status.py (P2, local review): the bundled twin is also hidden by the client's switch for that skill, CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL: registerClaudeApiSkill is called with
  disabled: env.CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL === true in the installed 2.1.296 binary (the function that initializes the bundled skills; neither the documentation nor the changelog at the pin names it).
  Measured in 14 init-record runs (receipts/skill-switch-init-records.json): 77 skills listed, 76 with the switch, the bundled copy gone and the plugin's kept; the spellings are the all-bundled switch's, and the
  settings env entry overrides the process environment in both directions. A bundled twin of another name has no such switch; a personal twin is not hidden by it.
- scripts/skills_status.py (P2, Astra, follow-up 2): with the plugin not listed (disabled, or not registered) and the bundled twin hidden (skillOverrides, the all-bundled switch or the skill's own) no route lists
  the skill: the twin entry is not_listed and the row fails. A disabled plugin with the bundled copy visible is one copy and passes.
- The push gate's read inventory (Astra's blocker 1) was answered by f7e0cd7 (the delta, 346 reads); this commit changes no count (the module passes on the committed tree, 127 tests).
- Truth fixes: the record says what is reported (never an install path or a credential) and what is read (the plugin's SKILL.md inside the plugin cache); the receipts README names the base that the older
  fail-before receipt was taken against.
- 14 of 14 mutants killed (receipts/mutants-round2.txt). Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…v entry is read as the client reads it, and a missing settings file is empty settings

The local review of the rebased head found two states in which the checker said ok or duplicate_listing where the client's listing is otherwise, and the command center ruled a checker that reports a wrong
state a gate defect: one final bounded commit, pushed with the registry-only rebase onto main 24a2090. Each case was reproduced on 7c7d667 and has a test that fails there
(receipts/fail-before-round3.txt).

- scripts/skills_status.py env_text (P2-1, :482-484): the client reads a settings env value as JavaScript's String() of the parsed JSON, so a switch spelled 1.0, 1e0 or [1] is set as 1 is. The reader
  took only Python int and bool and str, so 1.0 and 1e0 (floats) were no text and the switch was reported unset. Measured on 2.1.296 in 28 init-record runs
  (receipts/numeric-env-init-records.json): 61 skills listed, the bundled claude-api gone, with 1, 1.0, 1e0, 10e-1, 0.1e1, 1E0, 1.00, true, "1", [1], ["1"], [true] and [[1]]; 77 with 0, 0.0, 2, 1.5, -0,
  null, [], an object, [1, 2], ["1", "x"], [null], [null, 1] and [1, true]; the skill's own switch reads 1.0 and 1e0 the same way. env_text now writes a number as its integral value without a point or an
  exponent below 1e21, an array as its elements joined by commas, an object as "[object Object]"; an array nested beyond the interpreter's depth is no switch.
- scripts/skills_status.py read_switches (P2-2, :514-516): a settings file that does not exist is empty settings, so the process environment's CLAUDE_CODE_DISABLE_BUNDLED_SKILLS (or the skill's own
  switch) still counts, as the client starts with none and reads its environment; with no file no plugin is enabled either, so a hidden bundled copy is not_listed and the row fails instead of passing. A
  file that exists and cannot be read still disables nothing.
- tests/test_skills_status.py: test_a_number_or_an_array_in_an_env_entry_is_the_text_javascript_makes_of_it (27 subtests of raw settings text through the reader and the row's twin state, three for the
  skill's own switch, the deep array) and test_a_missing_settings_file_is_empty_settings_and_the_environment_switch_still_counts; 12 of 12 mutants killed (receipts/mutants-round3-12.txt, the first run
  left an array read as its first element only, and the test now holds the measured multi-element spellings).
- The push gate's read inventory is not changed by this commit (the module passes on the committed tree); the rebase re-derived the delta of the round-1 commit over main's tree (347 reads).
- Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/cc-native-practice-plugin-skill-identity-20261013 branch from f92f1ee to c17f820 Compare October 13, 2026 21:10
seathatflowsinourveins added a commit that referenced this pull request Oct 13, 2026
…client reads it, a built-in is of the builtin marketplace, and the tag resolution is read under the plugin's own repository (CPA-V-11)

The command center's read of e9a75c5 (record pr1092-claude-read-e9a75c5d.md, 2026-10-13T22:35Z; no P1, privacy clean) ruled one commit by this lane, in one push with the registry-only rebase onto main: P2-1,
P3-2 and P3-3 are fixed here, the rest are follow-ups.

- tests/test_plugin_pins.py, P2-1: enabled_plugins mirrors the 2.1.296 client's rule, `function lM(e){return e===!0||Array.isArray(e)}` and `aet`, which builds the enabled set with it (bytes 215984845 and
  215984892 of the binary, read here): `true` or a list, an empty list included; false, a string, a number, null and an object enable nothing. A list-enabled plugin with no entry passed all seven tests before.
  adoption/plugins/README.md says what is counted.
- P3-2: a `builtin` entry requires the `builtin` marketplace.
- P3-3: tag_commit reads the rows below the repository's own header line, up to the next header or a blank line (the capture lists its repositories back to back, so the rows of the repository listed next were
  read too, and the first text that ends with the repository's name was taken); is_sha uses fullmatch, which a 41-character value no longer passes.
- Fail-first: three new tests for the helpers, and receipts/mutants-round1-29.txt: the six mutants of the round (a plugin enabled by a list, by an empty list, in the additions template; a built-in of another
  marketplace; the pinned commit under a look-alike header and under the repository listed next) survive the pushed head's module and are killed by this one; five controls (false, a string, a number, null,
  an object) stay green in both; the 17 mutants of the first head are still killed.
- docs/decisions/2026-10-13-plugin-pins-stage-1.md: "Review round 1" with the follow-ups of the read (P2-2 jointly with #1074's: the registry reader of bootstrap.md and CLAUDE_CODE_PLUGIN_CACHE_DIR; P3-1, P3-4,
  P3-5, P3-6); the evidence README: one row.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins merged commit ef2fea1 into main Oct 13, 2026
31 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/cc-native-practice-plugin-skill-identity-20261013 branch October 13, 2026 23:49
seathatflowsinourveins added a commit that referenced this pull request Oct 14, 2026
…client reads it, a built-in is of the builtin marketplace, and the tag resolution is read under the plugin's own repository (CPA-V-11)

The command center's read of e9a75c5 (record pr1092-claude-read-e9a75c5d.md, 2026-10-13T22:35Z; no P1, privacy clean) ruled one commit by this lane, in one push with the registry-only rebase onto main: P2-1,
P3-2 and P3-3 are fixed here, the rest are follow-ups.

- tests/test_plugin_pins.py, P2-1: enabled_plugins mirrors the 2.1.296 client's rule, `function lM(e){return e===!0||Array.isArray(e)}` and `aet`, which builds the enabled set with it (bytes 215984845 and
  215984892 of the binary, read here): `true` or a list, an empty list included; false, a string, a number, null and an object enable nothing. A list-enabled plugin with no entry passed all seven tests before.
  adoption/plugins/README.md says what is counted.
- P3-2: a `builtin` entry requires the `builtin` marketplace.
- P3-3: tag_commit reads the rows below the repository's own header line, up to the next header or a blank line (the capture lists its repositories back to back, so the rows of the repository listed next were
  read too, and the first text that ends with the repository's name was taken); is_sha uses fullmatch, which a 41-character value no longer passes.
- Fail-first: three new tests for the helpers, and receipts/mutants-round1-29.txt: the six mutants of the round (a plugin enabled by a list, by an empty list, in the additions template; a built-in of another
  marketplace; the pinned commit under a look-alike header and under the repository listed next) survive the pushed head's module and are killed by this one; five controls (false, a string, a number, null,
  an object) stay green in both; the 17 mutants of the first head are still killed.
- docs/decisions/2026-10-13-plugin-pins-stage-1.md: "Review round 1" with the follow-ups of the read (P2-2 jointly with #1074's: the registry reader of bootstrap.md and CLAUDE_CODE_PLUGIN_CACHE_DIR; P3-1, P3-4,
  P3-5, P3-6); the evidence README: one row.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant