Find a file
Shaojin Wen 6b729f91d6
fix(core): gate skill announcements on what the model was declared (#9718)
* fix(core): gate skill announcements on what the model was declared

`CoreToolScheduler`'s skill-activation reminder exists to avoid announcing
a skill to a model that cannot invoke one — its own comment says so, and
names the case: "subagents ... may run with a restricted toolsList that
excludes SkillTool."

It read the wrong source. `registerLazy(ToolNames.SKILL, …)` carries no
`forSubAgent` guard and `prepareTools`' `warmAll()` loads it, so
`toolRegistry.getTool(SKILL)` is true for every subagent regardless of
what its `tools` list declares. The gate was therefore permanently open —
the condition it was written to detect could never be observed.

Two consequences, both quiet. The agent receives a `<system-reminder>`
naming a tool absent from its declarations and burns a turn discovering
that (`Tool "skill" not found`); and the announcement is marked consumed
on the SkillManager the parent shares, so the orchestrator that does hold
the tool never learns the skill activated.

The scheduler now takes an optional `hasSkillTool` predicate and prefers
it over the registry. `AgentCore` passes `willHaveSkillTool()` — the same
predicate that already decides whether to inject the startup
`<available_skills>` snapshot — so the snapshot and the per-tool-call
reminder answer from one source and cannot drift apart. Omitted, the
behaviour is unchanged, which keeps every existing owner (including the
top-level session, where declarations follow the registry) exactly as it
was.

The bundled `review-agent` type is the first shipped consumer to hit this:
six tools, none of them SKILL. `Explore` declares SKILL and
`statusline-setup` was reachable before it.

* fix(core): answer the skill gate from the declarations, not from toolConfig

Three findings from the review of this PR; the first is a defect in the
fix itself.

**The predicate was a copy of one filter, not the result of all of them.**
`willHaveSkillTool()` reads `toolConfig.tools`, so it cannot see the
`disallowedTools` blocklist, an inline-only declaration set, or a tool the
permission layer kept out of the registry. Each makes it answer "declared"
while `prepareTools()` excluded the tool — the same stale-copy shape as
the registry read this PR replaces, one level in. `prepareTools()` now
records the names it returned and the gate reads those, so every filter is
accounted for by construction rather than by a second implementation of
them. The pre-`prepareTools()` window keeps `willHaveSkillTool()`, which
is what the startup snapshot already uses; the doc comment says plainly
that it is an approximation and must not be used where the exact answer
exists.

**The announcing test proved less than it looked.** It set both the
declaration and the registry to true, so an implementation that AND-ed
them would have passed. It now sets the registry to FALSE while declaring
the tool, which only an implementation that actually prefers the
declaration survives.

**The gate's second effect was unpinned.** Suppressing the reminder is
half of it; the other half is leaving the announcement unconsumed, since
the orchestrator's drain consumes exactly those keys — a restricted
subagent that marks them used hides the activation from the owner that
can act on it. Moving `addInlineAnnouncedSkillKeys` outside the gate
passed the whole suite (374 passed). The harness now exposes that mock and
both directions are asserted; the same mutation is 1 failed | 373 passed.

Note on the first item's test: asserting only the recorded set left the
gate free to read something else — reverting the predicate stayed green.
The predicate is a named method now so a test can read the answer rather
than the record, and that revert is red.

* fix(core): sync the skill harness return type with what it returns

CI's `tsc --build` caught what the test run could not: exposing the
`addInlineAnnouncedSkillKeys` mock updated the `return` statement but not
the helper's explicit return-type annotation, so the three call sites
destructuring it failed with TS2353/TS2339.

Vitest does not typecheck, so the suite was green locally on a build that
cannot compile. `npm run build --workspace=packages/core` — the command
CI actually runs — is the one that reproduces it; `build:packages` is not
a substitute.

All three red checks came from this: the unit job and the web-shell E2E
job both failed in `Install dependencies` on the same build, and the
coverage job failed downstream with no artifact to download.

* fix(core): gate skill announcements on declared AND executable

Four findings on the last round; the first is a hole in the fix.

**Declared is not sufficient.** A fork keeps the parent's declared names
in `toolConfig.tools` for prompt-cache parity while `fork_tools` narrows
what may actually run, so `skill` can sit in the declarations and still be
refused at call time. The gate opened, the reminder invited an invocation
the execution allowlist then rejected, and the key was consumed on the
shared Config either way — both halves of the harm this change exists to
stop. The predicate is `declared AND executable` now, and the state
survives restart (`executionAllowedTools` is persisted and rebuilt), so
the fix belongs at the predicate rather than at spawn time.

**The authoritative set was already in scope.** `processFunctionCalls`
computes `declaredToolNames` from the very list sent to the model, about
150 lines above the scheduler it builds. The instance memo added last
round duplicated it under the same name — so a bare `declaredToolNames`
inside that method bound the local, not the field — and required every
future `prepareTools` return path to be wrapped or the gate silently fell
back. Gone: the field, the wrapper, both wrapped returns, and the
fallback branch. The gate closes over the local.

**Two comments claimed an invariant the last round falsified.** They said
the gate and the startup snapshot cannot disagree; the snapshot answers
from configuration before declarations exist, so with `tools: ['*']` plus
`disallowedTools: ['skill']` it says yes where the gate says no. Both now
state the real relation: the gate refines the snapshot and is never more
permissive.

On the tests: the first version asserted the two inputs separately, and
BOTH mutations survived it — dropping the execution term and reverting to
`willHaveSkillTool()` each left it green. Checking inputs is not checking
how they are combined. The gate is a named method so a test can read the
answer, and both mutations are now red.

* docs(core): one home for the skill-gate rationale, and drop a false claim

Three findings, and they share a cause: the same ~20-line rationale was
copied to three sites, so each revision had to update all three and one
always fell behind.

**A dead paragraph.** The wiring comment still described a
`willHaveSkillTool()` fallback removed two revisions ago, sitting between
two overlapping "Answered from…" paragraphs and contradicting the one
above it — which says deriving from `toolConfig` "would be a second copy
of those filters", while `willHaveSkillTool()` is exactly that read.

**The duplication itself.** The option docblock and the use site 4,000
lines apart carried the same reasoning near-verbatim, with a third
partial copy in the wiring. Both non-canonical sites are one-line
pointers now; the docblock keeps the rationale.

**A claim that was simply false.** Both copies asserted the gate "refines
the snapshot's answer and is never more permissive". It does not:
`willHaveSkillTool` reads only the STRING entries of `toolConfig.tools`,
so `['read_file', { name: 'skill' }]` gives snapshot=false while
`prepareTools` passes the inline declaration through and the gate says
true. Verified by probe before rewriting. The two predicates are
independent, not ordered, and the doc says so with an example of each
direction.

Both directions are now pinned by tests, because a claim with no test
behind it is how the earlier ones went stale: an ordering was asserted,
the mechanism changed, and the assertion outlived it.

* fix(core): remove a file that belongs to the companion PR

`InProcessBackend.test.ts` was staged from the wrong branch: the
`markRebuilt` call-site assertion it carries pins a change that lives in
the MCP-invariant PR, and neither the option nor the caller it asserts
exists on this branch. It compiled only because the import resolves
against `tools/agent/agent.js` either way, and it passed only because the
marker happens to be absent here for a different reason.

Reverted to its base state. The assertion is added on the branch that
introduces what it asserts.
2026-08-23 00:56:19 +00:00
.github fix(autofix): give the repair pass a budget it can finish in (#9691) 2026-08-23 00:32:36 +00:00
.husky Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
.qwen feat(autofix): audit the approach instead of stopping on growth-budget breach (#9262) 2026-08-21 04:54:07 +00:00
.vscode Merge branch 'main' into feat/sandbox-config-improvements 2026-03-06 14:38:39 +08:00
docs feat(cli): enable dynamic workflows from a settings key (#9098) 2026-08-23 00:33:51 +00:00
docs-site Hide internal docs from docs site (#4357) 2026-06-01 15:55:14 +08:00
eslint-rules refactor(core): remove root barrel self-imports and enforce the boundary (#9635) 2026-08-22 13:42:10 +00:00
integration-tests fix(cli): Recover sessions across archive races (#9513) 2026-08-22 14:01:59 +00:00
integrations/external-context chore(release): v0.22.0 (#9736) 2026-08-22 15:23:02 +00:00
packages fix(core): gate skill announcements on what the model was declared (#9718) 2026-08-23 00:56:19 +00:00
patches feat(cli): add TUI image display tool (#8217) 2026-08-01 12:39:52 +00:00
scripts fix(autofix): give the repair pass a budget it can finish in (#9691) 2026-08-23 00:32:36 +00:00
.dockerignore fix(cli): skip stdin read for ACP mode 2026-03-27 11:47:01 +00:00
.editorconfig pre-release commit 2025-07-22 23:26:01 +08:00
.gitattributes feat(installer): add standalone hosted install and uninstall flow (#3828) 2026-05-21 11:57:10 +08:00
.gitignore chore(ci): Add security hygiene: CODEOWNERS for release workflows, least-privilege permissions, security checks and Scorecard (#9008) 2026-08-14 01:22:53 +00:00
.npmrc chore: remove google registry 2025-08-08 20:45:54 +08:00
.nvmrc chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
.prettierignore feat(acp): support /cd command in ACP sessions (#5903) 2026-06-27 14:47:40 +00:00
.prettierrc.json pre-release commit 2025-07-22 23:26:01 +08:00
.yamllint.yml feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
AGENTS.md fix(devx): fail with actionable message when unit-test build prerequisites are missing (#9149) (#9171) 2026-08-18 13:19:09 +00:00
CHANGELOG.md chore(release): v0.22.0 (#9736) 2026-08-22 15:23:02 +00:00
CLAUDE.md docs: rewrite CLAUDE.md to point to AGENTS.md as authoritative source (#5138) 2026-06-15 15:23:26 +08:00
CONTRIBUTING.md revert: remove local PR verification gate (#7031) 2026-07-16 11:24:38 +00:00
Dockerfile perf(ci): cut the E2E suite from ~40min to ~24min (#7798) 2026-07-28 12:56:34 +00:00
esbuild.config.js chore(deps): Clear high-severity CVE baseline and harden the security gate (#9584) 2026-08-21 07:43:32 +00:00
eslint.config.js refactor(core): remove root barrel self-imports and enforce the boundary (#9635) 2026-08-22 13:42:10 +00:00
eslint.legacy-filenames.mjs feat(workflows): add cooperative pause and resume (#8320) 2026-08-08 04:21:21 +00:00
LICENSE Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
Makefile feat: update docs 2025-12-22 21:11:33 +08:00
package-lock.json chore(release): v0.22.0 (#9736) 2026-08-22 15:23:02 +00:00
package.json chore(release): v0.22.0 (#9736) 2026-08-22 15:23:02 +00:00
README.md docs(readme): add Korean to the documentation language bar (#8836) 2026-08-10 07:34:11 +00:00
SECURITY.md fix: update security vulnerability reporting channel 2026-02-24 14:22:47 +08:00
tsconfig.json # 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483) 2025-09-01 14:48:55 +08:00
vitest.config.ts feat(channel): add QQ Bot (QQ机器人) channel adapter (#5202) 2026-06-19 06:32:52 +08:00

npm version License Node.js Version Downloads

QwenLM%2Fqwen-code | Trendshift

The open-source AI coding agent that lives in your terminal.

中文 | Deutsch | français | 日本語 | Русский | Português (Brasil) | 한국어

Why Qwen Code?

  • Agentic out of the box — Auto-Memory, Auto-Skills, SubAgents, Agent Teams, and MCP. Dynamic workflows, zero setup.
  • Open-source, inside and out — The framework and the Qwen models are open-source. They evolve together. No vendor lock-in.
  • Multi-protocol — Supports OpenAI, Anthropic, Gemini, and Qwen APIs. Any third-party provider or local model (Ollama / vLLM). Switch at runtime.
  • Beyond the terminal — IDE plugins, Desktop app, daemon mode, SDKs, and IM bots (Telegram / DingTalk / WeChat / Feishu).

Tip

Qwen Code is actively iterating on itself — using its own agent and models to file issues, submit PRs, review code, and run tests. Powered by the community, driven by AI.

Installation

Linux / macOS:

curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash

Windows:

irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex

Restart your terminal after installation to ensure environment variables take effect.

NPM / Homebrew

NPM (requires Node.js 22+):

npm install -g @qwen-code/qwen-code@latest

Homebrew (macOS / Linux):

brew install qwen-code

Quick Start

qwen          # Launch interactive terminal UI
# Inside the session:
/auth         # Configure your provider and API key

See the Authentication Guide and Settings Reference for detailed setup.

Qwen Code

How to Use Qwen Code

Mode Command Use Case
Interactive qwen Terminal UI with rich rendering, @file references, slash commands
Headless qwen -p "..." Scripts, CI/CD, batch processing — no UI
IDE VS Code, Zed, JetBrains
Desktop Qwen Code Desktop — GUI for macOS, Windows, Linux
Daemon qwen serve Shared agent session over HTTP+SSE (ACP). Multiple clients, one agent. (experimental) Docs
SDK TypeScript, Python, Java
IM Bot qwen channel Connect to Telegram, DingTalk, WeChat, or Feishu
SDK example (Python)
import asyncio

from qwen_code_sdk import is_sdk_result_message, query


async def main() -> None:
    result = query(
        "Summarize the repository layout.",
        {
            "cwd": "/path/to/project",
            "path_to_qwen_executable": "qwen",
        },
    )

    async for message in result:
        if is_sdk_result_message(message):
            print(message["result"])


asyncio.run(main())

Capabilities

If you know Claude Code, you already know Qwen Code — and then some. We've put significant effort into bringing Qwen Code to feature parity with Claude Code, improving both breadth and reliability across the board.

Feature Qwen Code Claude Code
SubAgents, Agent Teams, Dynamic Workflows
Auto-Memory, Auto-Skills, Hooks
Built-in Skills (/review, /batch, /loop, /bugfix…)
MCP, Plan Mode, LSP Integration
Auto Mode, Sandbox, Git Worktrees
Computer Use (desktop automation)
IDE Plugins (VS Code / JetBrains / Zed)
SDK
Headless Mode, Session Management
Open-source — model and framework
Multi-protocol (OpenAI / Anthropic / Gemini / Qwen + any provider)
Agent Arena (multi-model head-to-head on same task)
Daemon Mode — qwen serve (multi-client shared agent)
IM Channels (Telegram / DingTalk / WeChat / Feishu)

Ecosystem

  • Qwen Code Desktop — Official desktop app for macOS, Windows, and Linux

  • AionUi — A modern GUI for command-line AI tools including Qwen Code

  • Gemini CLI Desktop — A cross-platform desktop/web/mobile UI for Qwen Code

  • 🦞 Qwen Code Claw — Let other agents (Claude, Codex, etc.) delegate coding tasks to Qwen Code via ACP. Paste this prompt into your agent:

Use Qwen Code as a coding agent. First, fetch and read this skill reference:
https://raw.githubusercontent.com/QwenLM/qwen-code/refs/heads/main/.qwen/skills/qwen-code-claw/SKILL.md
Then follow the instructions inside to install, authenticate, and use Qwen Code via acpx for all coding tasks.
  • Aliyun Model Studio CLI — Official CLI for Aliyun's AI platform (bailian-cli). Extends Qwen Code with image/video generation, knowledge retrieval, app orchestration, and model deployment

Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

Acknowledgments

This project was originally based on Google Gemini CLI v0.8.2. We gratefully acknowledge the Gemini CLI team's excellent work. Starting from Qwen Code v0.1, we stopped syncing with upstream and began independent development as a multi-protocol, multi-platform agent framework with deep integrations for Qwen models and beyond.