Repository navigation
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 intoOct 13, 2026
Conversation
seathatflowsinourveins
force-pushed
the
claude/cc-native-practice-plugin-skill-identity-20261013
branch
from
October 13, 2026 19:07
7c7d667 to
f92f1ee
Compare
…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
force-pushed
the
claude/cc-native-practice-plugin-skill-identity-20261013
branch
from
October 13, 2026 21:10
f92f1ee to
c17f820
Compare
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]>
5 tasks done
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope
claude-practice-audit-converged-20261012, P2):typesafe-aiandclaude-apiare each delivered to Claude Code by two routes and listed twice with one description, andskillOverridescannot 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.pyreportsduplicate_listingwhere 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 theclaude-apicurrency comparison is recorded. The host's ownskillOverridesentries are the command center's act (the diff is in the decision record and below).24a209016178de9850f9b36d351dc72f5b1b0a26(main after fix(trading): share strict paper URL normalization across credential gates #1080; rebased for landing from993cc890b3ff3fc95fa7cdba51edd726565f9ace, main after Keep lane-A journal continuity for session-only config changes #1068, the head when this was rebased for opening).lane:foundationadoption/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 ofslots.json),tests/test_skills_manifest.py,tests/test_skills_status.py,tests/test_skill_usage_daily.py,manifests/evidence.json.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 off7e0cd703) and round 3, the final bounded commit of the local review of the rebased head (below). On24a209016(the rebase below) they are3ec111b26,e3469d5dc,013459679,5a6635370andf92f1eeec; the reads and rounds above name the earlier heads.The change
typesafe-aicarriesclaude_plugin: typesafe@typesafe-aiandclaude_skill_name: typesafe:typesafe-ai;claude-apikeepsnative_pluginandclaude_skill_name: claude-api:claude-apiand gainsbundled_twin: claude-api(the bare name the client lists its own copy under).load_manifestchecks the fields (a plugin id is<plugin>@<marketplace>;claude_pluginandnative_pluginexclude each other;bundled_twinbelongs on anative_pluginrow;claude_skill_nameis<plugin>:<skill>)installed_plugins.jsonandenabledPluginstrue insettings.json) the other copy must be hidden: aclaude_twinentry,okorduplicate_listing(also for askillOverridesof the wrong type); the personal copy's expected listing on such a host isoff. A host without the plugin is unchanged. Ids and states onlyclaude_rowadds 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 useinstructions-skills.md,slots.json) states the plugin exception, what was measured and where the manifest and the check carry itcatalogs/foundation/upstream-surface-dispositions.json(:920to:923, twice) andcatalogs/foundation/new-wsl-architecture-20261001.json(:75to:76,:100to:103) cite, and the practice page's notes change the hash ofslots.jsonthat the snapshot label ofclaude-native-slot-manifest.jsonrecords (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)typesafe-ai: on, noclaude-apientry: a fresh host installs one copy), the Codex route, the installersSOTA sources
skillOverrides. Manage those through/plugininstead."), L928 and L333, fetched 2026-10-13T10:29:20Z (107,130 bytes, sha25635fd2c11f870fbe1497a53796de31b2a6b760645e724bda58894525356d4602a;receipts/docs-skills-md-excerpt.txt).CHANGELOG.mdL5319: "skillOverridessetting now works:offhides from model and/,user-invocable-onlyhides from model only,name-onlycollapses description".--settingsoverlays (receipts/listing-init-records.json) and the folder it unpacks for the bundledclaude-apiskill (receipts/claude-api-currency.txt).README.md: the Claude Code plugin route (claude plugin install typesafe@typesafe-ai) and the skills.sh route for other agents..claude-plugin/marketplace.jsonL46: theclaude-apiplugin entry the manifest row pins; itsclaude-apifolder is the plugin side of the currency comparison.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'sclaude_skill_name), the audit's CPA-V-07 text.Evidence-class table
<plugin>:<skill>; askillOverridesentry 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)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 abovereceipts/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)claude-apicopy 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 wordingreceipts/claude-api-currency.txt(file hashes and difflib summaries)skillOverridesentriesreceipts/host-overlay-simulation.json(the host's real settings read, the entries substituted in memory, nothing written)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)PluginTwinTests.test_the_practice_page_and_its_json_state_the_plugin_exception(the audit's fail-first page test)Local commands run
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
2dae97511is CHANGES_REQUESTED (verdict filefull1-pr1074-2dae97511/VERDICT.md, sha25650828557...) and the local codex review said "patch is incorrect" (three P2). The command center asked for one commit for the six; each was reproduced on2dae97511and has a test that fails there.2dae97511merges clean against mainba6afd8d7(exit status read directly): a plain fast-forward.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)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 killedscripts/skills_status.py:495-499: a host that disables bundled skills as a whole has one copy ofclaude-apiand was reported as a duplicatedisableBundledSkillstrue orCLAUDE_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 padded1(not 0, 2, false, off, no, y, t, enabled or empty); the settingsenventry overrides the process environment in both directions; documented incode.claude.com/docs/en/env-vars(receipts/docs-env-vars-excerpt.txt) andanthropics/claude-code@3deb873687379f0c199a69fdaccc65f0865f7a29CHANGELOG.mdL4500. A personal twin stays a duplicatetest_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_nothingtools/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 agonever,today,N days, and the older table'sN hours ago) and the most recent is taken; a cell that cannot be ordered makes the date unknowntest_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_unknownscripts/skills_status.py:471-474: a registry row withscope: userwas enough, even at another commit or with its cached skill goneplugin_status: pin, folder, bytes); an installed and enabled plugin that is not isplugin_invalid, hidden twin or nottest_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: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 blankCLAUDE_CONFIG_DIRsent them to different foldersclaude_config_dir: the client's configuration folder, whichCLAUDE_CONFIG_DIRmoves with "all settings, session history, and plugins"); the native plugin status reads it tootest_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:759-760: a Claude-only copied row withclaude_plugintook the copied-skill branch, which never computed the twintest_a_claude_only_copied_skill_gets_the_twin_policy_tooThe cases were reproduced on
2dae97511and pass on this head (receipts/fail-before-round1.txt: 14 tests fail or raise there); the original fail-first tests replayed at the exact base993cc890bgive the same 22 of 142 (receipts/fail-before-at-main-993cc890b.txt, Astra's P3 6, which also relabelsreceipts/fail-before.txt); 30 of 30 mutants of the round's changes are killed (receipts/mutants-round1-30.txt). The new helper sits besideclaude_row, so no line number cited by another decision record moves (the first version put it near the top:test_skill_usagecaught the shift ofledger_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.pyexit 0 (11,899 files);tests.test_skills_manifest38,tests.test_skills_status98,tests.test_skill_usage_daily18,tests.test_plugin_skill_identity_receipts3 OK; the 16 modules that name a changed file or a derived family (test_skill_usage221,test_upstream_surface_watch116,test_adoption_docs_consistency39,test_new_wsl_handbook88,test_validate86,test_published_digest_privacy15,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 weretest_skill_usage(the locator shift above, fixed) andtest_token_e2e_grader(a test clone hard-links across devices: the same failure at every earlier head);git merge-tree --write-tree origin/main HEADclean against mainba6afd8d7(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_statusand removed the twin check's own registry reader, so the OpenHands push gate's derivation counts two reads fewer inscripts/skills_status.py(346 against the 348 that main's newest delta expects); no read is added.tests.test_runtime_worker_openhands_push_gatederives its reads from the committed tree and I had run it on the working tree: it passed there and failed on the committed48e083e88, so the delta is its own commit:blueprints/runtime-workers/openhands/evidence/unclassified-gate-reads-20261013-pr1074-delta.jsonand its index entry (removed_by_script:scripts/skills_status.py2; the fourtools/credentials/set_credential.pyreads are #1057's, restated because the newest delta is cumulative). Atf7e0cd703:python3 scripts/validate.pyexit 0 (11,900 files),tests.test_runtime_worker_openhands_push_gate127 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 of48e083e8(one P2 blocker, answered; one adjacent P2)The local codex review of
f7e0cd70said "patch is incorrect" (one P2) and Astra's read of48e083e8(verdict filefix1-pr1074-48e083e88/VERDICT.md, sha25632f16c49...) is CHANGES_REQUESTED on one P2 thatf7e0cd703had already answered and one adjacent P2. The co-op asked for one commit for the two visibility cases; each was reproduced onf7e0cd703and has a test that fails there.scripts/skills_status.py:526-527: withCLAUDE_CODE_DISABLE_CLAUDE_API_SKILL=1and the pinned plugin enabled the client lists only the plugin's copy, and the check saw onlyskillOverridesand the all-bundled switch, so it reportedduplicate_listingfor a valid single-copy hostBUNDLED_SKILL_SWITCHES, keyed by the bundled name). Source: the installed 2.1.296 binary, the function that initializes the bundled skills (registerClaudeApiSkillis called withdisabled: 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 settingsenventry overrides the process environment in both directionstest_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):525,:735,:767: a disabled native plugin with the bundled twin hidden passed although no route lists the skillskillOverrides, the all-bundled switch or the skill's own, the twin entry isnot_listedand 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 checktest_a_bundled_twin_that_no_route_lists_is_not_listed_and_fails_the_row(four ways to hide, an unregistered plugin, and the controls)f7e0cd703(the deltaunclassified-gate-reads-20261013-pr1074-delta.json, 346 reads,removed_by_scriptscripts/skills_status.py2, 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_gate127 OK on the committed7c7d667f1, and the body's earlier claim that it passed at48e083e88was wrong and is corrected in the section abovetests.test_runtime_worker_openhands_push_gateon the committed headSKILL.mdat the registry's install path, inside the plugin cache)receipts/fail-before-round2.txt(2 tests fail onf7e0cd703, 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.pyexit 0 (11,903 files);tests.test_skills_manifest38,tests.test_skills_status100,tests.test_skill_usage_daily18,tests.test_plugin_skill_identity_receipts3 OK;tests.test_runtime_worker_openhands_push_gate127 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 and77cc7ef42#1075): this branch now conflicts with it onmanifests/evidence.jsonand 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
24a209016and review round 3 (the local review of the rebased head, two P2; one final bounded commit, pushed with the rebase)The pull request went
DIRTYonmanifests/evidence.jsonand the advisory delta index as main moved (#1070 appended a delta of its own forscripts/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_filefor the rows the commit changed; the delta of the one commit that carried one is re-derived with the nativeGitTreeandderive_ci_protectedover 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.git patch-id --verbatimof the-U0 --binarydiff against each side's own merge base: old head7c7d667f1against993cc890b, rebased head5a6635370against24a209016): 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.jsonand 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.tools/credentials/set_credential.py4 andscripts/validate.py1, with the scopes those deltas gave them;scripts/skills_status.py2 removed);source_commitis null as before (derived from the local commit that precedes the delta; the published head holds the same scripts).scripts/skills_status.py:482-484:env_texttook a number only as a Pythonint, so a JSON1.0or1e0(a float) was no text and the switch was reported unset, where the client sets itenv_textis JavaScript'sString()of the JSON value, as the client reads anenventry. Measured on 2.1.296 in 28 init-record runs (receipts/numeric-env-init-records.json): 61 skills listed, the bundledclaude-apigone, 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 1e0test_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 on7c7d667f1)scripts/skills_status.py:514-516:read_switchesreturned 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 copynot_listedand fails; a file that exists and cannot be read still disables nothingtest_a_missing_settings_file_is_empty_settings_and_the_environment_switch_still_countsThe cases were reproduced on
7c7d667f1and 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 7c7d667f1exit 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_status102,tests.test_skills_manifest38,tests.test_skill_usage_daily18,tests.test_skill_usage221,tests.test_plugin_skill_identity_receipts3,tests.test_adoption_status141,tests.test_adoption_docs_consistency39,tests.test_validate86,tests.test_registry_rows25 andtests.test_runtime_worker_openhands_push_gate127 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 cleanorigin/maincheckout, and the workflow files are not changed by this pull request; the local premerge check (a throw-away merge of the head onto main04f64a056):git merge-tree --write-treeexit status 0, the push-gate, skills-status and registry-rows modules 254 OK underGITHUB_ACTIONS=true,scripts/validate.pyexit 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
DIRTYonmanifests/evidence.jsonand the advisory delta index. It was rebased once, pushed with a lease onf92f1eeec, 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_filefor the rows the commit changed, the delta re-derived with the nativeGitTreeandderive_ci_protectedover the tree that now holds #1062's reads). The merge-tree check before the rebase conflicted in those two files and no other.git patch-id --verbatimof the-U0 --binarydiff against each side's own merge base: old headf92f1eeecagainst24a209016, new headc17f820d9against329985de1): 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.jsonand 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.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.scripts/skills_status.pythat this change removes);source_commitis null as before (derived from the local commit that precedes the delta; the published head holds the same scripts).c17f820d9(idle-class):python3 scripts/validate.py --registration-base origin/main --registration-before f92f1eeecexit 0 (11,942 files hashed);python3 scripts/evidence_manifest.py --writechanges nothing;git merge-tree --write-tree origin/main HEADexit 0;tests.test_runtime_worker_openhands_push_gate127,tests.test_registry_rows25,tests.test_skills_status102,tests.test_skills_manifest38 andtests.test_validate86 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, theclaude-apicurrency 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
permissions: {}and grant each job only what it needs. None changed.Host step for the command center (nothing is applied by this PR)
~/.claude/settings.json,skillOverrides: set"typesafe-ai": "off"(it is"on") and add"claude-api": "off". The simulated result isokfor the whole status;python3 scripts/skills_status.pyshowstwin=okfor both rows afterwards, and the client lists one copy of each (75 skills in the measured run).tools/adoption/apply_claude_settings.pysetstypesafe-aiback toon(the template wins on shared keys), and the status then reports the duplicate again; repeat step 1, or see the follow-up.Follow-ups
scripts/skills_status.py:518,env_switchstrips the value with Python'sstr.strip(), where the client trims with JavaScript'strim(); 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, thoughtrim()removes it, and a trailing U+0085 ("1\^E") reads as set, thoughtrim()keeps it. Not measured on the client for these two characters (the earlier padding measurements used ASCII spaces). On the next touch: strip withJS_TRIM_CHARS, with regressions both ways ("\ufeff1"sets the switch,"1\^E"does not).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 atc17f820d; recordpr1074-claude-read-and-adjudication-f92f1eee.md, sha256b1e923d153080faec0cbaf6bcf7890a802ea82ee9e4b69e46cf3498024812a10):okin a non-default setup:scripts/skills_status.pyreadsplugins/installed_plugins.jsonunder the config directory (claude_config_dir,:290-294; the reads at:775and:784), where the 2.1.296 client resolves its plugin folder fromCLAUDE_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 reportsok. 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.--add-dirfolders; the record's not-covered list names installs only. Follow-up: document it or implement it.claude-apirow passing. Follow-up: an explicit state for it.scripts/adoption_status.pysettings_env_text(:441-444) reads anenventry only as a string or an integer, so a JSON1.0for 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 asreceipts/numeric-env-init-records.json).offplus 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, andoffwithout 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.receipts/claude-api-currency.txtat each re-pin of theclaude-apirow and when a client release changes the bundled skill (the overturn condition).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.2dae97511: (P2, adjacent; corrected in round 2: a disabled plugin with the bundled twin hidden isnot_listed) the plugin row's own pass for a disabled plugin whose bundled copy is visible stays: one copy, the client's, andcanonicalis about the plugin's files, not its enablement; (P3) project and local scope installs and the profile replay (apply_claude_settings.pyrestorestypesafe-ai: on) stay the recorded boundaries; (P3, inherited)catalogs/sota-convergence/manifest-20260926.json:6521anchors a mutation-testing deferral atadoption/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).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