* 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.
|
||
|---|---|---|
| .github | ||
| .husky | ||
| .qwen | ||
| .vscode | ||
| docs | ||
| docs-site | ||
| eslint-rules | ||
| integration-tests | ||
| integrations/external-context | ||
| packages | ||
| patches | ||
| scripts | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| .npmrc | ||
| .nvmrc | ||
| .prettierignore | ||
| .prettierrc.json | ||
| .yamllint.yml | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| Dockerfile | ||
| esbuild.config.js | ||
| eslint.config.js | ||
| eslint.legacy-filenames.mjs | ||
| LICENSE | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
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.
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.
