fix(cli): Ensure knowledge/ citations and cross-skill references resolve - #87
Open
MajorLift wants to merge 16 commits into
Open
fix(cli): Ensure knowledge/ citations and cross-skill references resolve#87MajorLift wants to merge 16 commits into
knowledge/ citations and cross-skill references resolve#87MajorLift wants to merge 16 commits into
Conversation
knowledge/ once per domain, not once per skillknowledge/ references resolve on disk
knowledge/ references resolve on diskknowledge/ installations resolve
MajorLift
marked this pull request as ready for review
July 30, 2026 18:03
This was referenced Jul 30, 2026
Three reference kinds could name something no reader can reach, and none were checked: - Lane ids (`B7`, `C4`) are addresses into `evidence-catalog.md`, not names. A `description` cannot link out to the catalog at all, so a lane id there is unresolvable by construction — that is an error. In the body it is a warning unless the line links the catalog. - `[[snake_case]]` wiki links come from a private authoring vault and render as literal brackets here. Matching on the underscore keeps JS array literals (`[[signer1.address, …]]`) from tripping the rule. - Names in `## Related` must resolve to a skill. A warning rather than an error: the gate runs on the PR's own branch, where a sibling skill shipping in a concurrent PR does not exist yet. `collectSkills` takes an array of roots; passing a bare string iterates its characters and yields zero skills, which would have made every check above pass vacuously.
There was a problem hiding this comment.
Pull request overview
Adds CI-level protections and documentation to prevent silent breakage where installed skills reference knowledge/… files that aren’t actually present in the installed output, and to validate that in-repo knowledge/… citations resolve within their own domain.
Changes:
- Adds an install-time guard test that installs a fixture domain and asserts
knowledge/…references in emitted skill outputs resolve on disk. - Adds a corpus-wide test that scans skills for
knowledge/…citations and fails on any that the skill’s own domain cannot deliver (with an allowlist for known cross-domain cases). - Documents the correct way to refer to domain knowledge files in skills (installed-relative
knowledge/<file>.mdor by name; never repo-relative).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| test/cli.test.mjs | Adds new test guards for installed knowledge reference resolution and corpus validation of knowledge citations. |
| README.md | Documents the knowledge layout difference between repo and installed output and the safe citation formats. |
| CONTRIBUTING.md | Adds contributor guidance linking to the README rule for knowledge citations. |
Suppressed comments (1)
test/cli.test.mjs:340
- The corpus citation sweep currently only detects knowledge references in Markdown links or backticks. There are existing plain-text references like
knowledge/testing-layers.md(e.g. indomains/testing/skills/unit-testing/skill.md) that won’t be validated, so new dangling citations in that format could slip through. Expand the matcher to also catch bareknowledge/<file>.mdoccurrences.
for (const m of body.matchAll(/\]\((knowledge\/[\w.-]+\.md)\)|`(knowledge\/[\w.-]+\.md)`/gu)) {
const ref = m[1] || m[2];
found.push({ rel, domain, ref, key: `${rel} → ${ref}` });
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+298
to
+300
| const body = readFileSync(path.join(skillDir, file), 'utf8'); | ||
| const refs = [...body.matchAll(/\]\((knowledge\/[\w.-]+)\)/gu)].map((m) => m[1]); | ||
| assert.ok(refs.length > 0, `${base}/${name}: expected a knowledge reference in the emitted body`); |
…nymously This repo is public, so naming a repository discloses that it exists, who owns it and roughly what it holds. A prohibition discloses exactly as much as a recommendation — "do not re-host to <personal repo>, it is private" publishes the name either way — so the rule is about the mention, not the sentiment attached to it. A skill shipped with a personal repo named in a warning, on that reasoning. The check is a request rather than a list. An owner allowlist was tried first and was wrong on its first run: it flagged `nock/nock` and `phishfort/phishfort-lists`, because "is this owner well known" is not the property that matters. The property is whether a reader who is not you can open the link, and an unauthenticated HEAD answers it exactly. Deliberately unauthenticated — a token would see private repos and pass them. `MetaMask` and `Consensys` are exempt: org repos are unreachable publicly but readable by colleagues, and naming them is a deliberate call rather than a leak. Two-arm verified: fires on `MajorLift/Reprise` in the tree that shipped it, silent on the tree with it removed. The first version scanned only skill.md and would have passed the violation that motivated it, which was in a references/ file.
Scoped to the whole repository rather than the PR's changed files: whether this repo names something a reader cannot open is a property of what it publishes, not of what a given PR touched, and the reference that motivated this had sat unnoticed through several PRs that edited the same file. `GH_TOKEN` and `GITHUB_TOKEN` are blanked for the step. The check must run as a stranger — an authenticated request resolves private repositories and passes them, which is the exact failure it exists to catch.
"Did the right skill load" is a question about a probabilistic event — a description matched by a model — so the only honest answer comes from the transcript rather than from the description. This reads one and reports what entered context, by route. Three routes leave three different traces (Skill tool call, slash command, and the loader's base-directory announcement on a description match). Counting one of them reads as silence: run against the session that produced this file, the Skill-tool count alone suggests the reasoning skills were used, and all three together show that not one of them loaded at any point. The second report is the deterministic one. A publish is UNGATED if no gate ran before it at all, and UNCHAINED if a gate ran earlier but not as the same command. The distinction is the finding: a gate that merely ran earlier proves nothing, because the verdict can be read after the write — which is exactly how a blocked artifact reached a public pull request in the session this was written from. Only `gate && publish` makes the shell enforce the dependency. The first version reported that session as clean on the generous rule. It is 195 unchained publishes on the strict one.
…sees The lane-id, wiki-link and `## Related` checks had no cases in a suite that already exercises this script heavily, so each was one edit away from silently matching nothing. Ten cases, each shown to fail against the behaviour it claims to guard rather than merely to pass. Six mutations, five caught by exactly the test that names them: offset zeroed, catalog-link suppression removed, catalog-owner exemption removed, wiki regex loosened to any token, `## Related` running to EOF. The sixth is the interesting one. Deleting `&& name !== skill.name` changes nothing, because the linted skill is collected from the same tree as the known-name set — its own name is always in that set, so the clause never decides the case it appears to. That test is kept for the behaviour and its comment now says which mechanism actually delivers it. The offset assertion derives the expected line by scanning the written file, not by repeating the linter's arithmetic. The frontmatter it strips is exactly what shifts the numbers, so a test that recomputed the offset the same way would agree with the defect it exists to catch. 71 pass / 0 fail; repo lint 0 errors.
`copy_domain_knowledge` ran per skill per operator, so a domain with K knowledge files and N skills wrote K*N copies each. `perps` shipped 108 files where 27 were needed; `performance` after the pending domain PRs would ship 168 for 21. Knowledge now installs once to `mms-<domain>-knowledge/`, a sibling of the domain's installed skills. An upgrade removes the per-skill copies an older install left behind, and the shared directory is registered as expected so `--prune-stale` leaves it alone. This also fixes cross-layer references. The per-skill copy sat as a sibling of `references/`, three levels from where it lives in the repo, so `../../../knowledge/x.md` resolved in the repo and broke once installed while `../knowledge/x.md` did the reverse — no relative path was correct in both, and nothing reported the breakage. README and CONTRIBUTING now state the rule: cite knowledge files by name, never by relative path.
Installing domain `knowledge/` once per domain deduplicated the delivered tree but stranded every skill-relative `knowledge/<file>.md` citation — 12 working references in `domains/perps` alone. The installed tree is generated on every sync, so the duplication it removed was not worth a breaking layout change. `tools/install` is restored byte-for-byte to its previous behavior. What remains is the part that had value independent of the layout: - A regression guard: every `knowledge/…` reference in an emitted skill must resolve on disk after install. Verified to FAIL against the reverted design with `dangling knowledge reference knowledge/alpha.md`, and to pass here — a guard that cannot fire is not a guard. - Its fixture carries a real consumer (a skill body that cites a knowledge file), because the previous fixture had none and was structurally unable to exhibit the regression while every assertion passed. - README and CONTRIBUTING now state the rule the layout difference forces: cite knowledge by name or by the installed-relative path, never a repo-relative one, which is broken in the delivered output with nothing reporting it.
The fixture guard proves `tools/install` places knowledge where a skill body expects it. It says nothing about whether the skills in this repo cite files their own domain actually ships — and six citations do not. `knowledge/` is copied per domain, so a skill can only cite its own domain's files. Four skills in `coding`, `perps`, and `pr-workflow` cite `knowledge/testing-layers.md`, which lives in `domains/testing/`. The installer has no way to deliver it into those domains, so the reference cannot resolve for any consumer on any operator. Five of the six sit in `repos/metamask-mobile.md` overlays, which is likely why they went unnoticed. Those six are listed in `KNOWN_UNRESOLVED` so the check lands green and blocks new breakage rather than merging red. A second test fails if an entry starts resolving, so the list can only shrink. Both directions verified to fire: a new dangling citation fails the first test naming the offending pair, and satisfying a listed citation fails the second.
Two checks that found real defects by hand, now standing. Both run under `yarn test`, need no network, and pass on the current corpus, so they gate new breakage rather than landing red. Personal references — this repo is public, so an absolute home path, a personal handle, or a private-repo name is both a leak and a reference no reader but its author can resolve. The path pattern is anchored to a boundary; an unanchored one matches `../pages/home/homepage`. Frozen-branch links — `metamask-extension` moved its default to `main`, but `develop` still exists with a last commit of 2026-01-15. Links to it load and serve stale source, which is worse than a 404 because nothing signals the age. `FROZEN_BRANCHES` is a list so more can be added as branches are retired. Both verified to fire on an injected violation, naming file, line, and reason. Deliberately not gated: requiring every `/blob/<branch>/` link to be SHA-pinned fires 19 times on existing content, and is the wrong rule anyway — a directory listing should track the default branch. Pin when a link is evidence for a claim; track the branch when it is a place to look.
The denylist named five of them — a home path, a handle, and three repository names — committed to a public repository. A denylist of private identifiers publishes every identifier it protects, so the test leaked precisely what it existed to prevent, and did so more completely than any single skill file had. Structural patterns describe a shape and stay inline: an absolute `/home` or `/Users` path, an ssh remote. Anything naming a particular person, host or repository now comes from `SKILLS_PRIVATE_PATTERNS` — a newline-separated list of regex sources supplied by CI secret or an untracked local file, so the corpus is checked without the corpus being published. A generic email pattern was tried and dropped: it fired on a third-party address in oh-my-opencode's documented config, which is a documentation example rather than a leak. Identity-shaped patterns belong in the configured list, where the person who owns the identity decides. Two-arm verified: fails on a planted reference with the pattern configured, passes with the reference removed.
Moving the denylist to `SKILLS_PRIVATE_PATTERNS` stopped the leak and stopped the check: nothing sets that variable, so the identifier arm matched nothing anywhere. A check that cannot fire is not a weaker check, it is an absent one wearing the name of a check. The identifiers most likely to leak belong to whoever is running, and the environment already knows who that is — `GITHUB_ACTOR` in CI, `USER` and `git config user.name` locally. Values shorter than four characters or on a generic list (runner, ubuntu, ci, node…) are dropped, since a two-letter username matches every file. `SKILLS_PRIVATE_PATTERNS` remains for anything else worth catching. Two-arm verified with no environment configured: fails on a planted reference to the running user's handle, passes with it removed.
The guard read the delimiter, not the citation. Anchoring `knowledge/` to a preceding `](` or backtick matched two of the three forms skills actually use, and — because the `../` sits between the delimiter and the path — none of the repo-relative ones. That is the form the README calls broken and says this test guards, so the one defect the check existed to catch was the one it could not express. `perps-write-ticket` carried two, under a heading reading "read installed". Both 404 in the installed tree. Fixed here; the check now reports them. Consuming the `../` prefix is what makes them visible. Each repo-relative path ends in a correct-looking `knowledge/<file>.md`, so a matcher that skips the prefix resolves the suffix, finds the file, and passes the citation it should reject — broadening the matcher without capturing the prefix would have hidden these two a second way. The fixture cited one file, as a Markdown link — the form the matcher already handled, so it agreed with the blind spot rather than exposing it. It now cites all three forms, and the assertion is the set found, not `length > 0`: a narrowed matcher returns a subset whose every member resolves, which the old assertion passed. Verified by restoring the link-only matcher: `alpha` found, `beta` and `gamma` missed, test red. Also pins `/bin/bash` at the one call site that reached it through PATH; the other four in this file already did. 47 pass / 0 fail.
The `mobile-testing` skill reaches the knowledge file as `../../knowledge/testing-layers.md`. That path resolves while browsing the repo and 404s in the installed tree, which is the only tree a reader gets. Each site already names the installed form beside it, so the source pointer carries nothing the reader can follow.
MajorLift
force-pushed
the
jongsun/fix/knowledge-installs-once-per-domain
branch
from
August 12, 2026 20:15
e317b17 to
004d5da
Compare
Absorbs #94. Knowledge is copied per domain, so a skill citing another domain's file received nothing — four skills across coding, perps and pr-workflow cite testing-layers.md, which lives in testing. resolve_foreign_knowledge() delivers it. The corpus test and the installer fix belong together: the test names those four in KNOWN_UNRESOLVED, and leaving them in separate PRs meant whichever landed second had to reconcile a list the other branch could not see. Its rationale is corrected here rather than the list emptied, because the two are different properties. The corpus rule is a source-tree convention — cite your own domain's knowledge, so the citation reads without knowing the installer. Delivery is what the operator receives. After this commit all four resolve for an operator and still break the convention, and the old comment claimed they could never resolve for any consumer or operator — an impossibility claim falsified by the same change that carried it. Falsifiable: stubbing resolve_foreign_knowledge fails both cross-domain tests; restoring passes 21/21. Suite 74/74, lint 0 errors.
main added extension-testing (#130) with two repo-relative knowledge citations — `../../knowledge/extension-testing-layers.md` and a three-level variant — which are correct in this repo and 404 once installed. Rewritten to the installed-relative form, the same fix this branch already applied to mobile-testing. This is the corpus check finding a real violation rather than a regression: the branch was verified at 74/74 against a base six commits stale, and CI ran it against a main where the violation existed. Suite 74/74 merged with main; lint 52 skills, 0 errors.
knowledge/ installations resolveknowledge/ citation in an installed skill body resolve
Absorbs #103. No file overlap with this branch and no conflict — the two were adjacent in theme rather than entangled, unlike #94, which shared test/cli.test.mjs and the same four citations. Brings three reference checks in the linter: bare lane ids cited without linking the catalog that defines them, [[snake_case]] wiki links from a private authoring vault, and `## Related` entries naming a skill that does not exist on the branch. Plus check-public-refs, which asserts every repository named in the corpus resolves anonymously, and skill-audit. Scope note for reviewers: the lane check has no corpus subject today. Zero matches across 166 skill markdown files here, and zero across 256 non-evidence files on #84's branch, because evidence-catalog.md — the only place [A-G]N vocabulary exists — lives on #84. It is fixture-tested and prospective; it fires the day a non-evidence skill cites a lane. Suite 84/84; lint 52 skills, 0 errors; check-public-refs 6/6 resolve. Falsifiable: stubbing LANE_ID fails 2 lint tests, restoring passes 33/33.
knowledge/ citation in an installed skill body resolveknowledge/ citations and cross-skill references resolve
knowledge/ citations and cross-skill references resolveknowledge/ citations and cross-skill references resolve
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.
Guards a property that breaks silently, and then makes it hold: every
knowledge/…reference in an installed skill body resolves on disk. The installer now delivers knowledge a skill cites from another domain, and new tests keep it that way at the installer and across the source tree.The hazard
The installed layout puts knowledge two levels below where this repo keeps it, so every citation is written against one placement or the other. Before this change that broke in two ways, neither of which reported anything.
Wrong shape. A repo-relative citation (
../../knowledge/x.md) is correct in the source tree and broken in the delivered output, and the reverse holds too. So any change to where the installer puts knowledge breaks every skill that cites it, with nothing reporting it before this change — anddomains/perps/skillsalone carries 13 knowledge citations.Never delivered. Knowledge was copied per domain, so a skill citing another domain's file received nothing at all. Six citation sites across four skills in
coding,perpsandpr-workflowciteknowledge/testing-layers.md, which lives indomains/testing/. Five of the six sit inrepos/metamask-mobile.mdoverlays, which is plausibly why the breakage went unnoticed.Before this change, either way the skill installed, the reference dangled, and the agent quietly could not open what the body pointed at.
What ships
The cross-domain copy (
tools/installL376–L442, tests attest/cli.test.mjsL545–L629, commit-pinned) — the installer scans a skill forknowledge/<file>.mdcitations its own domain cannot satisfy and copies the file in from whichever domain owns it. Existing citations start working unchanged — no new syntax to learn, and nothing is declared in frontmatter, so the context an agent must load stays bounded. A cross-domain reference stays a body link the agent chooses to follow.Overlays are scanned too. Scanning only
skill.mdreaches one of the four affected skills; the other three citetesting-layers.mdfrom arepos/…overlay.Ambiguity fails loudly. Resolution is by filename —
resolve_foreign_knowledgeglobs*/knowledge/<name>acrossdomains/, and the name is unambiguous across the eleven knowledge files indomains/on this branch. If that stops being true the reference is genuinely ambiguous, so the install exits non-zero and names the candidates rather than picking one.The installer guard (
test/cli.test.mjsL284–L365) — installs a fixture domain and asserts that everyknowledge/…reference in each emitted skill resolves on disk, across all three operator outputs.Its fixture carries a real consumer: a skill body that cites knowledge the way shipped skills do, in all three forms a citation takes — markdown link, inline code, bare prose. A fixture without a citation cannot exhibit a placement regression at all, and a fixture carrying only the form the matcher already handles cannot exhibit the matcher's blind spot, because the fixture and the matcher share it.
The set of references found is asserted for the same reason: a narrowed matcher finds a subset, every member of the subset resolves, and a resolves-only assertion stays green while coverage shrinks.
A corpus check (
test/cli.test.mjsL367–L452) — sweeps everyknowledge/…citation indomains/and fails on a repo-relative path, or on a file the citing skill's own domain does not ship. The installer guard proves the installer places knowledge correctly; it says nothing about whether the skills in this repo cite files their own domain ships.The
testing-layers.mdcitations are listed in the corpus check'sKNOWN_UNRESOLVEDallowlist even though the installer now delivers them, because delivery and citation hygiene are different properties. The corpus rule is a source-tree convention — cite your own domain's knowledge, so the citation reads without knowing the installer. Emptying the allowlist means movingtesting-layers.mdinto each citing domain, or agreeing cross-domain citation is fine and deleting the corpus check. Both are calls for #88 (skills cite knowledge files their domain does not ship), not for this PR.A companion test in the same block, at L441–L451, fails on any entry that starts resolving, so the allowlist can only shrink.
Two publication gates (
test/cli.test.mjsL454–L543) — one rejects a personal path, handle, or private-repo name in a skill, which in a public repo fails twice over: it leaks, and it is a dead reference for every reader but its author.A second rejects links into
develop, which is worse than a deleted branch: the link loads and hands the reader stale source with no signal.metamask-extensionmoved tomain;developstill resolves and last took a commit on 2026-01-15.The private identifiers themselves are not listed in the test. A denylist of private identifiers, committed to a public repo, publishes every identifier it protects. Structural patterns describing a shape are inline; anything naming a particular person or host is derived from the environment (
GITHUB_ACTORin CI, the git identity locally), withSKILLS_PRIVATE_PATTERNSavailable to add more from a secret or an untracked file.Docs — README and CONTRIBUTING state the rule the layout difference forces: cite knowledge by name, or by the installed-relative path, never a repo-relative one.
Evidence
The figures in the table below come from one scenario:
--repo metamask-mobile --domain coding,perps,pr-workflow,testing, run against this branch'sdomains/tree, counting everyknowledge/…occurrence in the emitted.claude/skills/*/SKILL.mdbodies — sites, not distinct targets, and only across the skills that selection emits, which is fewer thandomains/holds. The only variable is whichtools/installruns.tools/installfrom4b00c7d, whosetools/installis the same blobmainhas todayThe six that were dangling are the
testing-layers.mdsites incoding-guidelines(×3),perps-review-pr,pr-guidelinesandpr-readiness-check. All four skills receive the file in each of the three outputs, and none did before — the same run writes.cursor/rules(asRULE.md) and.agents/skills, and both count 3 / 9 → 9 / 9 too.A guard that cannot fire is not a guard, so all nine of the test cases this PR adds were run against a defect they should catch, across the eight runs below — the first defect trips two of them. The suite is 74 tests and passes 74/74 (
yarn test, i.e.node --test test/*.test.mjs) with nothing induced. Each row below is a separate run of the full suite against this branch, with one defect planted in the tree.kb/instead of beside the skillthe owning domain still gets its own knowledgeboth fail:dangling knowledge reference knowledge/alpha.md — the body cites it but install did not place it there.claude/skills: cross-domain knowledge not deliveredambiguous reference should fail the installtesting-layers.mdcopied intodomains/coding/knowledge/, so a listed citation resolvesthese citations now resolve — remove them from KNOWN_UNRESOLVEDcodingskillnew dangling knowledge citation(s) — a skill may only cite its own domain's knowledge../../knowledge/…path planted in one skilla `../knowledge/…` path resolves in this repo and 404s once installed/home/<user>/…path planted in one skillpersonal or private references must not ship in a public skillblob/develop/link planted in one skilllink points into a frozen branch — use the repo's default branch, or pin a SHAThe corpus check caught a real violation on
main— in a skill this PR does not touch — and it is fixed.mainmerged#130(unified extension-testing skill), whoseextension-testingskill cited a seconddomains/testing/knowledge file,extension-testing-layers.md, repo-relatively —skill.mdas../../knowledge/…,references/layers.mdas../../../knowledge/…— exactly the form the corpus check bans, so one test failed, naming four citations across two files. Dropping the prefix in both files clears all four, the same edit004d5da— Citeknowledge/testing-layers.mdwithout the repo-relative prefix already made fortesting-layers.md; that is what4797d98— Merge main, and fix the citations it introduced does, and the run on that commit reports the four citations resolved.Notes
CHANGELOG.mdentry, and which release it should land under.knowledge/orreferences/file is out of scope for both —domains/perps/knowledge/review-antipatterns.mdL111 citesknowledge/testing-layers.mdand still dangles for every perps skill exceptperps-review-pr, which cites the file in its own body and so receives it anyway. Widening the scan to delivered files is a one-line change tosources=(…), and belongs in a follow-up rather than here.Appendix — why not a symlink
A
--repo metamask-mobileinstall writes 57 copies of knowledge intoperps' two installed skills across the three operator outputs — 54 from the domain's own 9 files, plus 3 copies of thetesting-layers.mdthis PR now delivers. A per-skill symlink into one shared directory is the obvious fix. It does not hold up, on three counts.It deduplicates nothing that matters. A recursive walk following links sees the content once per skill plus once at the shared location:
Same N× duplication a copy produces, for an agent reading the tree. The symlink saves disk — the cheap resource, regenerated every sync — and saves nothing in the context window, the scarce one.
Relocation strands it. The link is relative, so copying an installed skill anywhere else breaks it:
The installed tree is regenerated wholesale on every sync, so a copied
mms-<skill>/is the only form of it that outlives one install — and that copy is exactly the one a symlink breaks.Windows fails as a wrong-type file, not an error. Git stores a symlink as a mode-
120000entry, so where symlink creation is unavailable — no Developer Mode, no elevation,core.symlinks=false— it checks out as a regular file containing the target path.knowledgebecomes a file, soknowledge/alpha.mdis untraversable whileexistsSyncon the parent returns true: a failure that reads as success at the only level anything checks.All three consumers ignore the install target as of 2026-08-20:
core,metamask-extensionandmetamask-mobileeach gitignore.claude/skills/and.agents/skills/, and.cursor/rules/— as.cursor/rules/*with an*.mdccarve-out on mobile, which still excludes every directory the installer writes. So this would arrive through the installer rather than through a checkout, which narrows who is exposed without changing the failure mode.This repo also ships no symlinks —
git ls-files -sreports no mode-120000entries — so it would be a new artifact class in the delivered tree.Conclusion. Copying is right: the installed tree is generated, the duplication costs only disk, and every alternative trades that for a silent failure mode. The cross-domain copy above extends it rather than replacing it, for the same reasons. If deduplication is revisited, rewriting citations as the installer emits each skill is the safer direction — it changes text, not filesystem semantics, and the installer guard would catch it going wrong.