Skip to content

feat(claude-skill-to-desktop): new tool to sync Claude skills to desktop apps#34

Merged
genesiscz merged 2 commits intomasterfrom
feat/claude-skill-to-desktop
Feb 18, 2026
Merged

feat(claude-skill-to-desktop): new tool to sync Claude skills to desktop apps#34
genesiscz merged 2 commits intomasterfrom
feat/claude-skill-to-desktop

Conversation

@genesiscz
Copy link
Owner

Summary

  • Adds claude-skill-to-desktop tool that syncs Claude Code skills to desktop Claude app
  • Interactive multiselect via @clack/prompts to pick which skills to install
  • Guards non-directories in findManifestPath, fixes description regex, uses rm+cp on reinstall
  • Also includes two pre-existing TypeScript fixes:
    • azure-devops/history-search: wiql was set to a plain object instead of calling buildCombinedQuery()
    • utils/markdown: adds type declaration for untyped turndown-plugin-gfm package

Test plan

  • tools claude-skill-to-desktop — interactive skill picker launches
  • Select skills, confirm they appear in desktop Claude app's skill list
  • Re-running reinstalls cleanly (rm+cp, no stale files)
  • bunx tsgo --noEmit — zero compilation errors

fix: guard non-dirs in findManifestPath, fix description regex, rm+cp on reinstall

feat: implement interactive skill sync with clack multiselect

feat: claude-skill-to-desktop tool complete — smoke tested

fix: resolve two pre-existing TypeScript compilation errors

- azure-devops/history-search: wiql was set to a plain object instead of
  calling buildCombinedQuery() when --current flag is used with assignedTo
- utils/markdown: add type declaration for untyped turndown-plugin-gfm package
Copilot AI review requested due to automatic review settings February 17, 2026 23:03
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

Warning

Rate limit exceeded

@genesiscz has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 49 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/claude-skill-to-desktop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @genesiscz, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new utility for managing Claude skills, enabling seamless synchronization between local development environments and the Claude Desktop application. It streamlines the process of installing and updating skills, enhancing the developer workflow for Claude skill creators. Additionally, it includes important TypeScript fixes that improve the robustness of existing Azure DevOps integration and provide better type support for markdown processing.

Highlights

  • New Tool: claude-skill-to-desktop: Introduced a new CLI tool designed to synchronize Claude Code skills from your local ~/.claude/skills/ directory to the Claude Desktop application's local skill registry. This tool supports interactive multiselect for choosing which skills to install.
  • Interactive Skill Selection: The new tool leverages @clack/prompts to provide an interactive multiselect interface, allowing users to easily pick and choose which skills to install or update in their Claude Desktop app.
  • Robust Skill Management: Enhanced the skill discovery and installation process by guarding against non-directory entries in findManifestPath, refining the description regex for parsing SKILL.md frontmatter, and implementing a clean rm+cp mechanism for reinstallation to prevent stale files.
  • TypeScript Fix: Azure DevOps History Search: Corrected an issue in the Azure DevOps history search command where the wiql variable was incorrectly assigned a plain object instead of calling the buildCombinedQuery() function, ensuring proper query construction.
  • TypeScript Type Declaration: turndown-plugin-gfm: Added a type declaration file for the untyped turndown-plugin-gfm package, improving type safety and developer experience when working with markdown utilities.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • docs/plans/2026-02-17-claude-skill-to-desktop.md
    • Added a detailed implementation plan for the new claude-skill-to-desktop tool, outlining its architecture, tech stack, and development tasks.
  • src/azure-devops/commands/history-search.ts
    • Fixed a bug where wiql was assigned a plain object instead of the result of buildCombinedQuery() when useCurrent was true and assignedToValue was present.
  • src/claude-skill-to-desktop/README.md
    • Added a README file providing usage instructions and important notes for the claude-skill-to-desktop tool.
  • src/claude-skill-to-desktop/index.ts
    • Implemented the main CLI logic for the claude-skill-to-desktop tool, including argument parsing, manifest discovery, interactive skill selection, and installation flow.
  • src/claude-skill-to-desktop/lib.ts
    • Created core utility functions for the claude-skill-to-desktop tool, handling paths, skill discovery, YAML frontmatter parsing, skill ID generation, and manifest updates.
  • src/utils/markdown/turndown-plugin-gfm.d.ts
    • Added a TypeScript declaration file to provide type definitions for the turndown-plugin-gfm package.
Activity
  • A detailed implementation plan for the claude-skill-to-desktop tool was created and documented.
  • The core logic for scaffolding the tool directory and types was implemented.
  • The interactive multiselect flow for skill installation was developed and integrated.
  • Type-checking and verification steps were performed using tsgo to ensure code quality.
  • Smoke tests were conducted to verify the tool's functionality and integration with the tools command.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a valuable new tool for synchronizing Claude skills to the desktop application. The implementation is well-structured, utilizing appropriate libraries and a clear command-line interface. The inclusion of fixes for other parts of the codebase is also appreciated. My review highlights a few areas for improvement in the new tool, including a potential issue where built-in skills could be overwritten, along with some suggestions to enhance usability and performance. Overall, this is a solid contribution.

Comment on lines +35 to +36
const manifest = readManifest(manifestPath);
const skills = discoverLocalSkills(manifest);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

There's a risk of overwriting built-in skills. The README.md states that skills with creatorType: "anthropic" are not overwritten, but the current implementation doesn't enforce this. If a local skill shares a name with a built-in one, this tool will overwrite the manifest entry. It would be safer to filter out these conflicting skills and notify the user.

    const manifest = readManifest(manifestPath);
    const allLocalSkills = discoverLocalSkills(manifest);

    const builtInConflicts = allLocalSkills.filter((s) => s.installedEntry?.creatorType === "anthropic");
    if (builtInConflicts.length > 0) {
        p.log.warn(
            `Ignoring local skills that would overwrite built-in skills: ${builtInConflicts.map((s) => s.name).join(", ")}`
        );
    }
    const skills = allLocalSkills.filter((s) => s.installedEntry?.creatorType !== "anthropic");

Copy link
Owner Author

Choose a reason for hiding this comment

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

@gemini-code-assist Fixed in 93fc641d — filter anthropic built-ins before multiselect (warn + skip) + throw in installSkill if name collides with a built-in entry.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's great to hear! I've reviewed the changes in commit 93fc641d and confirm that the issue regarding overwriting built-in skills has been effectively addressed. The implementation correctly filters out Anthropic built-in skills before the multiselect and includes a warning message, as well as an additional safeguard in installSkill to prevent overwriting them. Excellent work!

Comment on lines +250 to +251
cd /Users/Martin/Tresors/Projects/GenesisTools
bun run src/claude-skill-to-desktop/index.ts --help
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The example command here includes a hardcoded, user-specific file path (/Users/Martin/...). To ensure the documentation is clear and useful for all contributors, it's best to use a generic placeholder.

Suggested change
cd /Users/Martin/Tresors/Projects/GenesisTools
bun run src/claude-skill-to-desktop/index.ts --help
cd /path/to/your/project
bun run src/claude-skill-to-desktop/index.ts --help

Copy link
Owner Author

Choose a reason for hiding this comment

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

@gemini-code-assist Fixed in 93fc641d — replaced /Users/Martin/Tresors/Projects/GenesisTools with /path/to/your/project in the example command block.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great, thanks for addressing that!

label: s.installedEntry
? `${s.name} ${pc.dim("(already installed — will update)")}`
: s.name,
hint: s.description.length > 70 ? s.description.slice(0, 70) + "…" : s.description,
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The interactive multiselect doesn't pre-select skills that are already installed, which makes the process of updating existing skills less convenient for the user. The implementation plan correctly identified this as a desired feature. Adding the selected property to the options will align with the plan and improve usability.

                    hint: s.description.length > 70 ? s.description.slice(0, 70) + "…" : s.description,
                    selected: !!s.installedEntry,

Copy link
Owner Author

Choose a reason for hiding this comment

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

@gemini-code-assist Fixed in 93fc641d — added selected: !!s.installedEntry to each multiselect option to pre-select already-installed skills.

Comment on lines +84 to +96
for (const skill of toInstall) {
spinner.start(`Installing ${pc.cyan(skill.name)}...`);
try {
installSkill(skill, manifest, manifestPath);
results.push({ name: skill.name, isUpdate: !!skill.installedEntry });
spinner.stop(
`${pc.green("✓")} ${skill.name} ${skill.installedEntry ? pc.dim("(updated)") : pc.dim("(new)")}`
);
} catch (err) {
results.push({ name: skill.name, isUpdate: false, error: String(err) });
spinner.stop(`${pc.red("✗")} ${skill.name}: ${String(err)}`);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The manifest.json file is written to disk on every iteration of this loop. This is inefficient and can be slow if many skills are being installed at once due to repeated I/O operations. A better approach is to update the manifest object in memory within the loop and then write it to the file a single time after the loop completes.

This would involve a small refactoring:

  1. In lib.ts, modify installSkill() to only handle file copying and in-memory manifest updates, without writing to disk.
  2. In lib.ts, add a new writeManifest() function to handle the file-writing logic.
  3. In this file (index.ts), call the new writeManifest() function once after the installation loop if any skills were successfully installed.

Here is an example of how the code could be structured:

In src/claude-skill-to-desktop/lib.ts:

// Modify installSkill to remove file writing
export function installSkill(skill: LocalSkill, manifest: Manifest, manifestPath: string): SkillEntry {
    // ... (file copying logic remains the same) ...
    const idx = manifest.skills.findIndex((s) => s.name === skill.name);
    const entry: SkillEntry = {
        // ... (entry creation logic remains the same) ...
    };
    if (idx >= 0) {
        manifest.skills[idx] = entry;
    } else {
        manifest.skills.push(entry);
    }
    return entry;
}

// Add new function to write manifest
export function writeManifest(manifest: Manifest, manifestPath: string): void {
    manifest.lastUpdated = Date.now();
    writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
}

In src/claude-skill-to-desktop/index.ts (this file):

    // ... (after the for loop)
    if (succeeded.length > 0) {
        spinner.start("Updating manifest...");
        try {
            writeManifest(manifest, manifestPath);
            spinner.stop("Manifest updated.");
        } catch (err) {
            spinner.stop("Failed to write manifest.");
            p.log.error(String(err));
        }
    }
    // ... (outro)

Copy link
Owner Author

Choose a reason for hiding this comment

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

skip this

Copy link
Owner Author

Choose a reason for hiding this comment

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

@gemini-code-assist Fixed in 93fc641d — extracted writeManifest(), installSkill() now only updates the in-memory manifest, and a single writeManifest() call is made after the loop with spinner feedback.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's great to hear! I'm glad the suggestion was helpful and that the efficiency improvement has been implemented. Thank you for addressing it.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new CLI tool to sync Claude Code skills (~/.claude/skills) into the Claude Desktop app’s local skills registry on macOS, plus a couple of TypeScript fixes elsewhere.

Changes:

  • Introduces claude-skill-to-desktop (CLI + library + README) with interactive multiselect and install/reinstall behavior.
  • Fixes azure-devops/history-search WIQL query construction to call buildCombinedQuery(...).
  • Adds a local .d.ts declaration for turndown-plugin-gfm to satisfy TypeScript.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/utils/markdown/turndown-plugin-gfm.d.ts Adds missing typings for turndown-plugin-gfm used by markdown utilities.
src/claude-skill-to-desktop/lib.ts Implements discovery of desktop manifest, local skill discovery, and install (copy + manifest update).
src/claude-skill-to-desktop/index.ts Implements the CLI flow (list, all, interactive multiselect) and install loop.
src/claude-skill-to-desktop/README.md Documents usage and behavior notes for the new tool.
src/azure-devops/commands/history-search.ts Fixes incorrect WIQL value assignment to use buildCombinedQuery(...).
docs/plans/2026-02-17-claude-skill-to-desktop.md Adds an implementation plan document for the new tool.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +62 to +68
options: skills.map((s) => ({
value: s,
label: s.installedEntry
? `${s.name} ${pc.dim("(already installed — will update)")}`
: s.name,
hint: s.description.length > 70 ? s.description.slice(0, 70) + "…" : s.description,
})),
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The multiselect treats any existing manifest entry as "already installed — will update". If an installed entry is an Anthropic built-in (creatorType "anthropic"), the tool should not offer it as updatable (or should clearly mark it as blocked) to match the documented behavior and avoid confusing UX.

Suggested change
options: skills.map((s) => ({
value: s,
label: s.installedEntry
? `${s.name} ${pc.dim("(already installed — will update)")}`
: s.name,
hint: s.description.length > 70 ? s.description.slice(0, 70) + "…" : s.description,
})),
options: skills.map((s) => {
const isAnthropicBuiltIn = s.installedEntry?.creatorType === "anthropic";
const label = s.installedEntry
? isAnthropicBuiltIn
? `${s.name} ${pc.dim("(built-in — cannot update)")}`
: `${s.name} ${pc.dim("(already installed — will update)")}`
: s.name;
const hint =
s.description.length > 70 ? s.description.slice(0, 70) + "…" : s.description;
return {
value: s,
label,
hint,
disabled: isAnthropicBuiltIn ? "Built-in skills cannot be updated" : false,
};
}),

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 93fc641d — anthropic built-ins are filtered out before the multiselect (Thread #1 covers this); the label logic also handles the built-in case defensively showing "(built-in — cannot update)".

Comment on lines +89 to +93
const skillsDir = join(manifestPath, "..", "skills");
const destPath = join(skillsDir, skill.name);
if (existsSync(destPath)) rmSync(destPath, { recursive: true });
mkdirSync(destPath, { recursive: true });
cpSync(skill.sourcePath, destPath, { recursive: true });
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

skill.name comes from SKILL.md frontmatter and is used directly in join(skillsDir, skill.name). A crafted name like ../../something or an absolute path could escape the intended skills directory, leading to unintended deletes/copies via rmSync/cpSync and writing a misleading manifest entry. Validate/sanitize the skill name before using it as a path segment (e.g., reject path separators, .., and absolute paths; or resolve and assert the destination stays within skillsDir).

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 93fc641d — added path traversal guard using resolve() + startsWith(resolvedSkillsDir + sep) before rmSync/cpSync in installSkill().

Comment on lines +94 to +107
const idx = manifest.skills.findIndex((s) => s.name === skill.name);
const entry: SkillEntry = {
skillId: idx >= 0 ? manifest.skills[idx].skillId : generateSkillId(),
name: skill.name,
description: skill.description,
creatorType: "user",
updatedAt: new Date().toISOString(),
enabled: true,
};
if (idx >= 0) {
manifest.skills[idx] = entry;
} else {
manifest.skills.push(entry);
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

This overwrites any existing manifest entry that matches by name, including entries with creatorType: "anthropic". That conflicts with the README note and can clobber built-in skills if a user skill name collides. Before replacing manifest.skills[idx] (and before deleting/copying the skill folder), detect creatorType === "anthropic" and skip/throw with a clear message so built-ins are never overwritten.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 93fc641dinstallSkill() now throws if an existing manifest entry has creatorType === "anthropic" before any file operations or manifest modification.

Comment on lines +159 to +160
// Copy directory
const destPath = join(skillsDir, skill.name);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The planned installSkill implementation uses skill.name from SKILL.md frontmatter directly in join(skillsDir, skill.name) to build destPath for copying files, which allows names containing ../ segments or absolute paths to escape the intended Claude Desktop skills directory. A malicious or untrusted skill definition could set a crafted name so that running this tool (--all or interactive) writes into arbitrary locations in the user’s filesystem, potentially overwriting important files. To prevent this, ensure skill.name is sanitized to a single safe path segment (e.g., derived from dirName or validated to reject path separators and ..) before using it in filesystem paths.

Suggested change
// Copy directory
const destPath = join(skillsDir, skill.name);
// Copy directory (use dirName to ensure a safe, single path segment)
const destPath = join(skillsDir, skill.dirName);

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 93fc641d — plan doc updated to use skill.dirName with a note about path validation to prevent traversal.

- Guard against overwriting anthropic built-in skills (filter + lib-level throw)
- Pre-select already-installed skills in multiselect
- Write manifest once after loop instead of per iteration
- Sanitize skill.name against path traversal
- Fix hardcoded user path in docs
@genesiscz genesiscz merged commit 229f83f into master Feb 18, 2026
1 of 2 checks passed
@genesiscz genesiscz deleted the feat/claude-skill-to-desktop branch February 18, 2026 00:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants