Skip to content

feat(claude): Link global skills one by one so Claude Code keeps its synced skills - #195

Closed
m-naoki-m wants to merge 3 commits into
getsentry:mainfrom
m-naoki-m:feat/claude-user-skill-links
Closed

m-naoki-m wants to merge 3 commits into
getsentry:mainfrom
m-naoki-m:feat/claude-user-skill-links

Conversation

@m-naoki-m

Copy link
Copy Markdown
Contributor

Closes #193. Builds on #194, which this branch includes; the diff to review is the last commit once #194 is merged.

At global scope ~/.claude/skills/ is a link to ~/.agents/skills/, so Claude Code downloads the skills enabled on claude.ai (synced/, .trash/, .staging/) into the shared directory, and Codex and OpenCode load them as ordinary skills. This keeps ~/.claude/skills/ a real directory at global scope, for Claude Code and Cursor, and links each shared skill into it.

Changes

  • init, install, and sync link every skill in ~/.agents/skills/, including projected plugin skills, as ~/.claude/skills/<name>. Links whose skill is gone or lost its SKILL.md are removed, and remove unlinks right away, also for plugins and wildcard exclusions.
  • An existing directory link is replaced, and synced/, .trash/, and .staging/ move back to ~/.claude/skills/. Entries are only renamed, as in the existing migration, so on different file systems the command stops before moving anything. The new directory is built next to the link and swapped in once it is ready, and the next run finishes a conversion that was interrupted. If Claude Code already recreated one of those entries, the copy in the shared directory moves to ~/.agents/.client-owned-backup/, and init, install, sync, and doctor --fix print where. A ~/.claude/skills link that points elsewhere, or cannot be resolved and does not name the shared directory, stops the command.
  • sync and init move a skill directory created in ~/.claude/skills/ into ~/.agents/skills/ and link it back; sync also declares it. install and doctor --fix do not edit agents.toml, so they only link. Names that agents.toml declares or agents.lock records are never moved in either direction; those cases, and names present on both sides, are reported by install, sync, and doctor.
  • doctor reports a directory link and missing or stale links, which doctor --fix repairs. Skills not shared yet (run sync) and conflicts that need a decision are separate checks without a fix.
  • The bundled dotagents skill tells agents to create new skills in ~/.agents/skills/ and link them for Claude Code.
  • specs/SPEC.md, README.md, docs/public/llms.txt, and the skill's configuration reference describe the global layout.

Project scope keeps the directory link.

Verification

  • pnpm check on Node 20 and 26: lint, typecheck, and tests pass (lib 295, host 887). New tests cover conversion from a directory link, recovery after an interrupted conversion, sharing client-created skills, managed-name and client-owned-name conflicts, unresolvable links, plugin skills, pruning, remove, the install warnings, a declaration of synced written by an earlier sync, and recovering from a failed or interrupted conversion. Each fix from review was checked against a test that fails without it.
  • The CI build job steps (pnpm build, CLI --help, packing both packages, scripts/verify-pack.mjs) pass on Node 20, and the docs build passes on Node 22.
  • In the dotagents-qa Docker image: pnpm check and pnpm qa:example pass. A global fixture with a directory link and a synced/ inside the shared directory converts on sync; a skill created in ~/.claude/skills/ is shared and declared; install, remove, doctor, and doctor --fix behave as described; init on a home that already has ~/.claude/skills/synced/ keeps it in place. Claude Code, Codex, and OpenCode in the image load the shared skills. pnpm qa:plugins fails at the Copilot proof on main as well.
  • On my own machine (Claude Code 2.1.283, Codex CLI 0.156.0, OpenCode 1.18.31): after sync converted the directory link, Claude Code loaded the 31 shared skills and the 12 claude.ai skills and kept refreshing ~/.claude/skills/synced/, and Codex no longer listed the claude.ai skills. OpenCode still lists them, because it reads ~/.claude/skills/ itself.

m-naoki-m and others added 2 commits September 27, 2026 23:27
sync adopted every undeclared directory under the skills directory. A
hidden directory such as .trash/ was written to agents.toml with a name
the config schema rejects, so every later command, including doctor,
failed with 'Invalid config'. Directories without a SKILL.md, such as
Claude Code's synced/ after ~/.claude/skills/ is migrated, were adopted
as skills.

Adopt a directory only when its name is a valid skill name and it
contains a SKILL.md. Pruning of stale managed skills is unchanged. If a
directory adopted earlier loses its SKILL.md and its agents.toml entry,
drop its in-place agents.lock entry too, so .agents/.gitignore stops
hiding its files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…synced skills

At global scope ~/.claude/skills/ was a link to ~/.agents/skills/, so
Claude Code wrote the skills it syncs from claude.ai (synced/, .trash/,
.staging/) into the shared directory, where Codex and OpenCode loaded
them as ordinary skills.

Keep ~/.claude/skills/ a real directory for Claude Code and Cursor at
global scope and link each shared skill into it. An existing directory
link is converted by building the new directory next to it and swapping
it in; client-owned entries move back, and a duplicate the client
already recreated is set aside under ~/.agents/.client-owned-backup/.
sync and init share skills created in ~/.claude/skills/, and sync
declares them; install and doctor --fix only link. Names declared in
agents.toml or recorded in agents.lock are never moved, and sync drops a
synced declaration an earlier version wrote. Entries are only renamed,
so on different file systems the command stops before moving anything.
init, install, sync, and doctor report conflicts and set-aside entries,
and remove unlinks removed skills. Project scope keeps the directory
link.

The bundled dotagents skill now tells agents to create new skills in
the shared directory and link them for Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 27, 2026

Copy link
Copy Markdown

@m-naoki-m is attempting to deploy a commit to the Sentry Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the risk: high PR risk score: high label Sep 27, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f1a8d5e. Configure here.

Comment thread packages/dotagents/src/cli/commands/sync.ts
Comment thread packages/dotagents/src/symlinks/per-skill.ts
A skill a wildcard source expanded into agents.lock has no agents.toml
entry of its own name, so a name clash with a Claude Code skill was
described as a stale record that install would clear. Check against the
declared names, which include wildcard-expanded skills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gricha

gricha commented Sep 29, 2026

Copy link
Copy Markdown
Member

i think i may be missing the point here, dotagents is meant to unify this layer, and treating claude skills as sort of isolated entities defeats a lot of the prupose. i'm gonna close this one, but if you wanna reopen with more context/chat more lmk

@gricha gricha closed this Sep 29, 2026
@m-naoki-m

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look. I should have explained the motivation better in the description. The goal was to keep the shared layer, not to split Claude off from it.

The problem is what Claude Code writes into ~/.claude/skills/ on its own. Skills enabled on claude.ai are downloaded to ~/.claude/skills/synced/ and refreshed while Claude Code runs. With the global directory link, that folder lands inside ~/.agents/skills/, and Codex and OpenCode load those skills as their own. On my machine that added 12 claude.ai skills to Codex, and skill-creator and pdf appeared twice there.

In this PR every skill in ~/.agents/skills/ was still linked for Claude Code, and a skill created in ~/.claude/skills/ was moved into ~/.agents/skills/ on sync. Only synced/, .trash/, and .staging/, which Claude Code manages, stayed in ~/.claude/skills/.

I understand if per-skill links are not the right shape for dotagents. Do you see the claude.ai skills showing up in other agents as something worth fixing? If so, I'd be glad to rework it in a way that fits the project better. If it's expected behavior, I'm happy to close #193 as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Global Claude skills link exposes Claude Code's claude.ai-synced skills to Codex and OpenCode

2 participants