qwen-code/docs/users/extension/introduction.md
易良 0b953b7929
fix(core): support public GitHub extensions with older Git (#9690)
* fix(core): clarify Git requirement for public extensions

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(core): preserve secure Git version boundary

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): support public GitHub extensions with older Git

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): harden old-Git fallback archive validation against export-ignore

Detect Git LFS by pointer-file content instead of .gitattributes grammar: codeload archives honor export-ignore, so a repository can hide its attributes file from the extracted tree and slip raw LFS pointers past the guard (attribute macros and case-variant names bypass the grammar check too). Also restrict the .gitmodules check to the archive root, where git gives it submodule semantics. Add a debug log to the only silent ERROR return in the old-Git update check, unit tests for the fallback gate's fail-closed matrix, and coverage for the invalid-SHA update path.

* test(core): cover archive entry-count and expanded-size limits

Add crafted-header tar fixtures for both new rejection limits in assertTarArchiveHasNoLinks: boundary cases at exactly 100,000 entries and exactly 1 GiB declared expansion, plus just-over cases asserting the specific error messages.

* fix(core): address old-Git fallback review feedback

- Keep release installs ahead of the archive fallback for older Git;
  the fallback now only replaces the clone step after a release miss.
- Restrict tar entry-count/expanded-size ceilings to the untrusted
  network fallback instead of every .tar.gz extraction, and stop
  reading the archive as soon as validation fails.
- Share one ref-to-SHA resolver between install and update checks, and
  follow a limited number of GitHub API redirects (re-validated per
  hop, token never leaves the original host).
- Use a random staging name for the downloaded source archive so it
  cannot collide with a repository file of the same name.
- Collapse the duplicated pinned-Git version comparison into one check.
- Document fallback limitations (symlinks, submodules, LFS, ceilings).

* test(core): cover invalid commit SHA rejection in old-Git fallback

The install path's ref-to-SHA resolver validates the 40-hex SHA before
interpolating it into the codeload download URL; add a test asserting
that an invalid SHA rejects before any archive download is attempted,
matching the existing update-check coverage.

* fix(test): add missing createReadStream and pipeline mocks in npm test

archive-safety.ts now calls fs.createReadStream() and pipeline() directly
instead of tar.t({ file, ... }). The npm test mock for node:fs was missing
createReadStream, and node:stream/promises pipeline was not mocked.

* perf(core): memoize the local Git version probe

The fallback gate and the pinned-Git assert both spawn their own
`git version` subprocess even though the version cannot change within
a process lifetime. Fetch it once through a module-scope memoized
promise so each extension install/update check pays a single probe.

* test(core): cover early abort of the tar safety scan

Once a limit trips, the scan destroys the read stream instead of
consuming the rest of the archive. Add a regression test that trips
the link ceiling with a large trailing entry and asserts the scan
stops reading the archive at the failure point, guarding the teardown
path against deadlocks and scan-to-end regressions.

* fix(core): open the tar safety scan stream after the abort check

A pre-aborted signal entering assertTarArchiveHasNoLinks threw before
pipeline consumed the hoisted ReadStream, abandoning it (unhandled
ENOENT 'error' crash for a missing file, leaked fd otherwise). Move
createReadStream below the abort check to restore check-then-open
order, and add a regression test asserting no stream is opened.

* test(core): cover fetchJson redirects and fallback resource limits

Mirror the downloadFile redirect matrix for fetchJson via the release
metadata path: redirect loop cap, missing location header, non-https
redirect rejection, and both sides of the cross-host token-stripping
ternary. Also add a fallback integration test serving a crafted-header
archive just over the 1 GiB expanded ceiling so the enforceResourceLimits
option on the production call site is pinned end to end.

* fix(test): return a destroyable stream from the npm test fs mock

The bare createReadStream mock returned undefined, so failValidation's
stream.destroy() raised a TypeError absorbed by vitest spy bookkeeping
whenever a validation cap tripped. Return a destroyable object and
assert the cap-trip path completes cleanly.

* fix(test): make fallback anonymity assertions header-case-insensitive

* test(core): abort the old-Git fallback through an AbortSignal

* fix(test): pin the manager's fallback call arguments

* test(core): pin fallback symlink rejection, lookup passthrough, per-hop re-resolution

- Add an integration test that runs the real old-Git fallback against a
  symlink-bearing archive mirroring issue #8993's repro repo
  (obra/superpowers root AGENTS.md -> CLAUDE.md) and asserts the honest
  fail-closed rejection naming the link entry; safe symlink support is
  tracked in #9724.
- Assert the fallback's https.get options carry the pinned lookup and
  agent:false on both the commits-API and codeload hops.
- Run the five GitHub API redirect tests under networkPolicy: 'public'
  and pin per-hop re-validation: dns.lookup is called once per hop and
  every hop's options carry the pinned lookup.

* test(core): import archive limit constants instead of redeclaring them

The boundary tests redeclared MAX_ARCHIVE_ENTRIES and
MAX_ARCHIVE_EXPANDED_BYTES locally, so changing a limit in
archive-safety.ts would leave the tests validating the stale values.
Import the constants from the implementation instead.

* fix(core): detect export-ignore-hidden submodules via the commit tree

The submodule guard checked for a root-level .gitmodules in the
extracted archive, but codeload archives honor .gitattributes
export-ignore, so a repository can strip its .gitmodules from the
archive and slip past the presence check while still carrying
submodule gitlinks. Query the commit's tree listing, which keeps every
path regardless of export-ignore, and reject on a root .gitmodules blob
or any gitlink entry before downloading; fail closed when GitHub
truncates the listing. The extracted-tree scan stays as defense in
depth.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-23 13:03:15 +00:00

20 KiB

Qwen Code Extensions

Qwen Code extensions package prompts, MCP servers, subagents, skills and custom commands into a familiar and user-friendly format. With extensions, you can expand the capabilities of Qwen Code and share those capabilities with others. They are designed to be easily installable and shareable.

Extensions and plugins from Gemini CLI Extensions Gallery, Claude Code Marketplace, Qoder, and the portable Agent Plugins v1 format can be directly installed into Qwen Code. This cross-platform compatibility gives you access to a rich ecosystem of extensions and plugins, dramatically expanding Qwen Code's capabilities without requiring extension authors to maintain separate versions.

Extension management

We offer a suite of extension management tools using both qwen extensions CLI commands and /extensions slash commands within the interactive CLI.

Runtime Extension Management (Slash Commands)

You can manage extensions at runtime within the interactive CLI using /extensions slash commands. These commands support hot-reloading, meaning changes take effect immediately without restarting the application.

Command Description
/extensions or /extensions manage Manage all installed extensions
/extensions install <source> Install an extension from a git URL, local path or archive, archive URL, npm package, or marketplace
/extensions explore [source] Open extensions source page(Gemini or ClaudeCode) in your browser

The interactive extension manager

Running /extensions (or /extensions manage) opens an interactive manager with three tabs. Press Tab or the / arrows to switch between them.

  • Discover — browse plugins from your configured marketplace sources. Type to search, Enter to view a plugin's details, and install it (you'll be asked to choose an install scope). Press Ctrl+R to re-fetch the listings, and Esc to go back.
  • Installed — your installed extensions, grouped by scope (User level, Project level, and favorites). Use / to navigate, Space to enable/disable an extension, f to favorite it, and Enter to open its details. MCP servers bundled by an extension appear nested under their parent extension with live connection status; you can enable or disable each server individually from there.
  • Sources — manage the marketplace sources that feed the Discover tab. Use / to navigate, Enter to select a source, and d to remove one. These are the same sources managed by the qwen extensions sources CLI commands described below.

Changes made here hot-reload immediately, without restarting Qwen Code.

CLI Extension Management

You can also manage extensions using qwen extensions CLI commands. Note that changes made via CLI commands will be reflected in active CLI sessions on restart.

Installing an extension

You can install an extension using qwen extensions install from multiple sources:

From Claude Code Marketplace

Qwen Code also supports plugins from the Claude Code Marketplace. Install from a marketplace and choose a plugin:

qwen extensions install <marketplace-name>
# or
qwen extensions install <marketplace-github-url>

If you want to install a specific plugin, you can use the format with plugin name:

qwen extensions install <marketplace-name>:<plugin-name>
# or
qwen extensions install <marketplace-github-url>:<plugin-name>

For example, to install the prompts.chat plugin from the f/awesome-chatgpt-prompts marketplace:

qwen extensions install f/awesome-chatgpt-prompts:prompts.chat
# or
qwen extensions install https://github.com/f/awesome-chatgpt-prompts:prompts.chat

Claude plugins are automatically converted to Qwen Code format during installation:

  • claude-plugin.json is converted to qwen-extension.json
  • Agent configurations are converted to Qwen subagent format
  • Skill configurations are converted to Qwen skill format
  • Tool mappings are automatically handled

You can quickly browse available extensions from different marketplaces using the /extensions explore command:

# Open Gemini CLI Extensions marketplace
/extensions explore Gemini

# Open Claude Code marketplace
/extensions explore ClaudeCode

This command opens the respective marketplace in your default browser, allowing you to discover new extensions to enhance your Qwen Code experience.

Cross-Platform Compatibility: This allows you to leverage the rich extension ecosystems from both Gemini CLI and Claude Code, dramatically expanding the available functionality for Qwen Code users.

From Gemini CLI Extensions

Qwen Code fully supports extensions from the Gemini CLI Extensions Gallery. Simply install them using the git URL:

qwen extensions install <gemini-cli-extension-github-url>
# or
qwen extensions install <owner>/<repo>

Gemini extensions are automatically converted to Qwen Code format during installation:

  • gemini-extension.json is converted to qwen-extension.json
  • TOML command files are automatically migrated to Markdown format
  • MCP servers, context files, and settings are preserved

From Qoder Plugins

Qwen Code supports Qoder plugins that contain a .qoder-plugin/plugin.json manifest. Install a local directory, archive, Git repository, archive URL, or scoped npm package with the existing qwen extensions install command:

qwen extensions install ./sample-qoder-plugin
qwen extensions install ./sample-qoder-plugin.zip
qwen extensions install owner/sample-qoder-plugin

The installer converts the Qoder manifest to qwen-extension.json and preserves standard commands/, agents/, and skills/ directories. MCP servers declared in a root .mcp.json file are included as extension MCP servers.

When a Qoder plugin contains system-prompt.md at its root, Qwen Code loads it as extension context. If the plugin also contains QWEN.md or declares other context files, all context files are retained and deduplicated.

From Agent Plugins v1

Qwen Code natively loads portable Agent Plugins v1 packages without converting or rewriting plugin.json, mcp.json, or SKILL.md files:

qwen extensions install ./my-agent-plugin
qwen extensions link ./my-agent-plugin
qwen extensions install owner/my-agent-plugin

The portable runtime supports Agent Skills plus stdio and Streamable HTTP MCP servers. Commands, agents, hooks, client namespaces, and legacy SSE MCP are not activated. See Agent Plugins v1 for the complete support matrix.

From npm Registry

Qwen Code supports installing extensions from npm registries using scoped package names. This is ideal for teams with private registries that already have auth, versioning, and publishing infrastructure in place.

# Install the latest version
qwen extensions install @scope/my-extension

# Install a specific version
qwen extensions install @scope/my-extension@1.2.0

# Install from a custom registry
qwen extensions install @scope/my-extension --registry https://your-registry.com

Only scoped packages (@scope/package-name) are supported to avoid ambiguity with the owner/repo GitHub shorthand format.

Registry resolution follows this priority:

  1. --registry CLI flag (explicit override)
  2. Scoped registry from .npmrc (e.g. @scope:registry=https://...)
  3. Default registry from .npmrc
  4. Fallback: https://registry.npmjs.org/

Authentication is handled automatically via the NPM_TOKEN environment variable or registry-specific _authToken entries in your .npmrc file.

Note: npm extensions must include either a native qwen-extension.json or an Agent Plugins v1 plugin.json at the package root. See Extension Releasing for packaging details.

From Git Repository

Git 2.37 or newer is required for credentialed, non-GitHub, nested marketplace, submodule, and Git LFS sources because Qwen Code uses http.curloptResolve to pin Git connections to validated DNS results. On older Git versions, Qwen Code supports only anonymous public https://github.com/{owner}/{repo}[.git] root repositories by resolving the requested ref to a commit and downloading GitHub's source archive with the same public-network and archive-safety checks.

Because the older-Git fallback installs from a source archive rather than a clone, it cannot install repositories that rely on symlinks, submodules, or Git LFS, and it caps downloads at 100 MiB compressed and archives at 100,000 entries / 1 GiB expanded. Release-based installs are still preferred when a repository publishes releases.

qwen extensions install https://github.com/github/github-mcp-server

This will install the github mcp server extension.

From Local Path

qwen extensions install /path/to/your/extension

Local .zip and .tar.gz archives are also supported:

qwen extensions install /path/to/your/extension.zip
qwen extensions install /path/to/your/extension.tar.gz

The archive must contain a complete extension at its root, or a single top-level directory containing the extension.

Note that we create a copy of the installed extension, so you will need to run qwen extensions update to pull in changes from both locally-defined extensions and those on GitHub.

From Archive URL

qwen extensions install https://example.com/your/extension.zip
qwen extensions install https://example.com/your/extension.tar.gz

Archive URLs can be updated later as long as the URL continues to point at a newer archive for the same extension.

Choosing an install scope

By default, an installed extension is enabled globally (user scope). Pass --scope project to enable it only for the current workspace:

qwen extensions install <source> --scope project

--scope workspace is accepted as an alias of --scope project. This matches the scope choice offered when installing from the /extensions manage Discover tab.

Managing marketplace sources

Marketplace sources (Claude plugin marketplaces) power the Discover tab in /extensions manage. You can manage them from the CLI as well:

# Add a marketplace (owner/repo, git URL, https URL to marketplace.json, or local path)
qwen extensions sources add <source>

# List configured marketplaces
qwen extensions sources list

# Re-fetch a marketplace's plugin listing
qwen extensions sources update <name>

# Remove a marketplace
qwen extensions sources remove <name>

Uninstalling an extension

To uninstall, run qwen extensions uninstall extension-name, so, in the case of the install example:

qwen extensions uninstall qwen-cli-security

Disabling an extension

Extensions are, by default, enabled across all workspaces. You can disable an extension entirely or for specific workspace.

For example, qwen extensions disable extension-name will disable the extension at the user level, so it will be disabled everywhere. qwen extensions disable extension-name --scope=workspace will only disable the extension in the current workspace.

Enabling an extension

You can enable extensions using qwen extensions enable extension-name. You can also enable an extension for a specific workspace using qwen extensions enable extension-name --scope=workspace from within that workspace.

This is useful if you have an extension disabled at the top-level and only enabled in specific places.

Updating an extension

For extensions installed from a local path or archive, an archive URL, a git repository, or an npm registry, you can explicitly update to the latest version with qwen extensions update extension-name. For npm extensions installed without a version pin (e.g. @scope/pkg), updates check the latest dist-tag. For those installed with a specific dist-tag (e.g. @scope/pkg@beta), updates track that tag. Extensions pinned to an exact version (e.g. @scope/pkg@1.2.0) are always considered up-to-date.

You can update all extensions with:

qwen extensions update --all

How it works

On startup, Qwen Code looks for extensions in <home>/.qwen/extensions

Native Qwen extensions exist as a directory that contains a qwen-extension.json file. Agent Plugins v1 packages instead retain their root plugin.json; see Agent Plugins v1.

For example, a native Qwen extension is stored at:

<home>/.qwen/extensions/my-extension/qwen-extension.json

qwen-extension.json

The qwen-extension.json file contains the configuration for the extension. The file has the following structure:

{
  "name": "my-extension",
  "version": "1.0.0",
  "mcpServers": {
    "my-server": {
      "command": "node my-server.js"
    }
  },
  "channels": {
    "my-platform": {
      "entry": "dist/index.js",
      "displayName": "My Platform Channel"
    }
  },
  "contextFileName": "QWEN.md",
  "commands": "commands",
  "skills": "skills",
  "agents": "agents",
  "settings": [
    {
      "name": "API Key",
      "description": "Your API key for the service",
      "envVar": "MY_API_KEY",
      "sensitive": true
    }
  ]
}
  • name: The name of the extension. This is used to uniquely identify the extension and for conflict resolution when extension commands have the same name as user or project commands. The name should be lowercase or numbers and use dashes instead of underscores or spaces. This is how users will refer to your extension in the CLI. Note that we expect this name to match the extension directory name.
  • version: The version of the extension.
  • mcpServers: A map of MCP servers to configure. The key is the name of the server, and the value is the server configuration. These servers will be loaded on startup just like MCP servers configured in a settings.json file. If both an extension and a settings.json file configure an MCP server with the same name, the server defined in the settings.json file takes precedence.
    • Note that all MCP server configuration options are supported except for trust.
  • channels: A map of custom channel adapters. The key is the channel type name, and the value has an entry (path to compiled JS entry point) and optional displayName. The entry point must export a plugin object conforming to the ChannelPlugin interface. See Channel Plugins for a full guide.
  • contextFileName: The name of the file that contains the context for the extension. This will be used to load the context from the extension directory. If this property is not used but a QWEN.md file is present in your extension directory, then that file will be loaded.
  • commands: The directory containing custom commands (default: commands). Commands are .md files that define prompts.
  • skills: The directory containing custom skills (default: skills). Skills are discovered automatically and become available via the /skills command.
  • agents: The directory containing custom subagents (default: agents). Subagents are .yaml or .md files that define specialized AI assistants.
  • settings: An array of settings that the extension requires. When installing, users will be prompted to provide values for these settings. The values are stored securely and passed to MCP servers as environment variables.
    • Each setting has the following properties:
      • name: Display name for the setting
      • description: A description of what this setting is used for
      • envVar: The environment variable name that will be set
      • sensitive: Boolean indicating if the value should be hidden (e.g., API keys, passwords)

Managing Extension Settings

Extensions can require configuration through settings (such as API keys or credentials). These settings can be managed using the qwen extensions settings CLI command:

Set a setting value:

qwen extensions settings set <extension-name> <setting-name> [--scope user|workspace]

List all settings and current values for an extension:

qwen extensions settings list <extension-name>

Settings can be configured at two levels:

  • User level (default): Settings apply across all projects (~/.qwen/.env)
  • Workspace level: Settings apply only to the current project (.qwen/.env)

Workspace settings take precedence over user settings. Sensitive settings are stored securely and never displayed in plain text.

When Qwen Code starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence.

Custom commands

Extensions can provide custom commands by placing Markdown files in a commands/ subdirectory within the extension directory. These commands follow the same format as user and project custom commands and use standard naming conventions.

Note: The command format has been updated from TOML to Markdown. TOML files are deprecated but still supported. You can migrate existing TOML commands using the automatic migration prompt that appears when TOML files are detected.

Example

An extension named gcp with the following structure:

.qwen/extensions/gcp/
├── qwen-extension.json
└── commands/
    ├── deploy.md
    └── gcs/
        └── sync.md

Would provide these commands:

  • /deploy - Shows as [gcp] Custom command from deploy.md in help
  • /gcs:sync - Shows as [gcp] Custom command from sync.md in help

Custom skills

Extensions can provide custom skills by placing skill files in a skills/ subdirectory within the extension directory. Each skill should have a SKILL.md file with YAML frontmatter defining the skill's name and description.

Example

.qwen/extensions/my-extension/
├── qwen-extension.json
└── skills/
    └── pdf-processor/
        └── SKILL.md

The skill will be available via the /skills command when the extension is active.

Custom subagents

Extensions can provide custom subagents by placing agent configuration files in an agents/ subdirectory within the extension directory. Agents are defined using YAML or Markdown files.

Example

.qwen/extensions/my-extension/
├── qwen-extension.json
└── agents/
    └── testing-expert.yaml

Extension subagents appear in the subagent manager dialog under "Extension Agents" section.

Conflict resolution

Extension commands have the lowest precedence. When a conflict occurs with user or project commands:

  1. No conflict: Extension command uses its natural name (e.g., /deploy)
  2. With conflict: Extension command is renamed with the extension prefix (e.g., /gcp.deploy)

For example, if both a user and the gcp extension define a deploy command:

  • /deploy - Executes the user's deploy command
  • /gcp.deploy - Executes the extension's deploy command (marked with [gcp] tag)

Variables

Qwen Code extensions allow variable substitution in qwen-extension.json. This can be useful if e.g., you need the current directory to run an MCP server using "cwd": "${extensionPath}${/}run.ts".

Supported variables:

variable description
${extensionPath} The fully-qualified path of the extension in the user's filesystem e.g., '/Users/username/.qwen/extensions/example-extension'. This will not unwrap symlinks.
${workspacePath} The fully-qualified path of the current workspace.
${/} or ${pathSeparator} The path separator (differs per OS).