Find a file
jinye 174e8de179
fix(core): stop AbortSignal listener leak in long sessions (MaxListenersExceededWarning) (#4366)
* fix(core): consolidate AbortController handling to stop listener leaks in long sessions

Users hit `MaxListenersExceededWarning: 1509 abort listeners added to
[AbortSignal]` in long interactive sessions. The agent runtime nests
parent→child controllers (masterAbortController → per-message round →
per-API-call round → tool execution) and each layer registered its own
`addEventListener('abort', ...)` on the parent without `{once:true}` or
reverse cleanup, so listeners accumulated on long-lived parents across
hundreds of model turns.

Add `utils/abortController.ts` with three helpers:

- `createAbortController(maxListeners = 50)` — factory that pre-caps the
  signal so the warning never fires on per-request signals.
- `createChildAbortController(parent)` — WeakRef-based parent→child
  propagation with `{once:true}` on the parent listener AND a reverse-cleanup
  listener on the child that detaches the parent listener when the child
  aborts. This is the key mechanism — short-lived children stop accumulating
  dead listeners on long-lived parents.
- `combineAbortSignals(signals, {timeoutMs})` — N-way combiner that replaces
  the existing one-input `combinedAbortSignal.ts` (kept as a `@deprecated`
  shim so `httpHookRunner.ts` doesn't churn).

Migrate every production `new AbortController()` in `packages/core/src` (24
sites) to the helper. Wrap `_runReasoningLoopInner` per-iteration body and
`AgentHeadless.execute` in `try/finally` so the round controller is aborted
(triggering reverse cleanup) even when the model stream or tool execution
throws. Add `{once:true}` to the manual abort listeners in `hookRunner`,
`functionHookRunner`, and `message-bus` that were missing it. Remove the
`raiseAbortListenerCap` band-aid from `openaiContentGenerator/pipeline.ts` —
no longer needed now that the per-round signal carries `maxListeners=50`.

Add `cli/utils/warningHandler.ts` as a belt-and-suspenders: hides
`MaxListenersExceededWarning.*AbortSignal` from end users in production
(any shape Node ≥20 emits), keeps it visible under `DEBUG`/`QWEN_DEBUG`/
`NODE_ENV=development`. Uses `process.on('warning', ...)` without
`removeAllListeners` so third-party warning subscribers stay intact.

Direct reproducer in `docs/verification/abort-controller-refactor/` proves
the old pattern accumulates 2000 listeners over 2000 rounds while the new
pattern stays at 0.

* fix(core): address PR #4366 review feedback

Four issues from the Copilot review:

1. combineAbortSignals — add a per-iteration `aborted` check inside the
   for-loop so we short-circuit if an input signal flips aborted between
   the initial scan and listener registration. In single-threaded JS this
   can't actually interleave, but the defensive check makes correctness
   obvious and protects against signals whose `aborted` getter has side
   effects. New test exercises the path via a Proxy that flips after the
   initial scan.

2. warningHandler docstring — was stale: said "AbortSignal / EventTarget"
   while the regex was tightened to AbortSignal-only in the previous review.

3. README.md — replace personal absolute path with `$WT` placeholder so
   the verification recipe is shareable.

4. README.md — replace the markdown table with per-scenario headed
   sections. Prettier had interpreted an inline `ps -ef | grep sleep`
   pipe character as a column separator, breaking the table rendering on
   GitHub. Per-section format is also easier to scan and edit.

* test(core): fix abortController race-defense test to actually hit the loop check

The previous version set the Proxy's `aborted` to true before calling
combineAbortSignals, so the initial `find` scan caught it and we took the
fast path — not the per-iteration check the test was meant to validate.

Switch to an access counter so `aborted` is false on the first read (during
`find`) and true on subsequent reads (inside the loop). This forces the
loop to enter, then catches the flip via the defensive per-iteration check
before any listener is attached to the next input.

Verified the test fails if the per-iteration check is removed.

* fix(lint): include docs/**/*.mjs in the script ESLint block so the AbortController repro passes lint

CI Lint flagged 11 no-undef errors in
docs/verification/abort-controller-refactor/listener-accumulation-repro.mjs
(AbortController, console, process) because the project's flat config
only declared Node globals for ./scripts/**/*.mjs.

The reviewer's suggestion (`/* eslint-env node */`) doesn't work under
ESLint 9 flat config — env directives are deprecated there. The proper
fix is to extend the existing script-globals block to also cover the
verification repro script under docs/.

* fix(core,cli): address PR #4366 critical review findings

Two real bugs the reviewer caught and I confirmed locally:

1. warningHandler.ts didn't actually suppress anything. Adding a
   `process.on('warning')` listener does NOT prevent Node's default
   onWarning printer from writing to stderr — the default is just an
   ordinary listener registered in `lib/internal/process/warning.js`.
   My previous code therefore:
   - failed to suppress targeted AbortSignal warnings (they still hit
     stderr via the default printer)
   - produced a SECOND copy of every non-suppressed warning (default
     printer + my handler's own stderr.write)
   The unit tests missed it because they synthesised a fake warning and
   called `process.listeners('warning')` directly rather than going
   through `process.emitWarning`.

   Fix: snapshot the existing `'warning'` listeners (which include the
   default printer and any third-party telemetry hooks) BEFORE replacing
   them. Install ours as the sole listener. For non-suppressed warnings
   fan out to the captured set so the default printer + telemetry still
   fire; for suppressed warnings stop here. Tests now use
   `process.emit('warning', ...)` to drive the real listener chain, plus
   a spawned-child integration test that asserts the real stderr from
   `process.emitWarning` is empty for AbortSignal warnings and still
   contains DeprecationWarning text.

2. abortController.createChildAbortController kept a WeakRef to the
   child controller. A natural usage pattern — pass `child.signal` into
   an async API and drop the controller object — could let the
   controller be GC'd while the signal is still in use, after which
   `parent.abort()` would no longer propagate. Reproduced with
   `node --expose-gc`.

   Fix: hold the child strongly via the parent's listener closure. The
   reverse-cleanup listener still removes the closure when child aborts
   (closure releases child → GC-eligible), and the parent's `{once:true}`
   listener self-removes when parent fires (same effect). Net listener
   accounting on long-lived parents is unchanged; the only difference is
   the controller now stays alive long enough for propagation to reach
   downstream consumers that hold only the signal. Tests updated: drop
   the old `--expose-gc`-dependent assertion that abandoned children
   GC immediately (that was a property of the OLD contract); add a
   signal-only-retention test that verifies propagation under the new
   contract without needing GC at all.

Verified: 32 helper/warning tests pass (incl. spawned-child stderr
integration); 363 affected caller tests pass; typecheck + prettier +
eslint clean for the touched files.

* fix(core,cli): address PR #4366 review — fix combineAbortSignals orphan listeners + runtime DEBUG toggle

Two real bugs the reviewer caught:

1. combineAbortSignals registered its cleanup listener on
   controller.signal AFTER the for-loop. Node does NOT fire 'abort'
   listeners added to an already-aborted signal, so when the
   per-iteration defensive check aborted the controller mid-loop, the
   cleanup never ran — orphaning every input-signal listener registered
   before the break, and leaving the (also-registered-after-the-break)
   setTimeout uncleared.

   Fix: skip timeout scheduling when controller.signal.aborted is
   already true post-loop, and when it's true call cleanup()
   synchronously instead of registering a doomed listener. Existing
   test for the mid-iteration path now also asserts that the
   pre-break input signal (a) has zero abort listeners — that's the
   assertion that catches the orphan bug. New test for the
   already-aborted-input + timeoutMs combination confirms the timer
   isn't scheduled (would otherwise overwrite the abort reason).

2. warningHandler captured isDebugMode() in a closure at init time, so
   toggling DEBUG / QWEN_DEBUG at runtime (e.g. via a /debug slash
   command) didn't update suppression behavior. Moved the check inside
   the handler — warnings are rare so the per-emit env-lookup cost is
   negligible. New test asserts a mid-stream DEBUG=1 flip starts
   forwarding suppressed warnings to the prior-listener chain.

* test(core): strengthen the timeout-guard test in combineAbortSignals to actually exercise the new !aborted check

Reviewer correctly pointed out that the previous version of this test
took the pre-loop fast path (since `a.abort('pre')` ran before
`combineAbortSignals`), so it never reached the in-loop guard at
abortController.ts:138.

Switched to the Proxy `aborted`-getter pattern from the sibling
mid-iteration test (so the loop genuinely re-checks `aborted` and
short-circuits inside the for-loop), and added a `setTimeout` spy that
asserts the timer was never scheduled — this is the only observable
difference from "scheduled then immediately cleared by synchronous
cleanup()", which is what the timer-advance assertion alone couldn't
distinguish.

Verified by mutation testing: removing the guard makes the new test
fail; restoring it makes it pass. Refs PR #4366.

* test(core): cover timeout-triggered cleanup of input-signal listeners in combineAbortSignals

Reviewer noted the timeout path only had an empty-input test, leaving
the leak-sensitive case uncovered: when timeoutMs fires with a
long-lived source signal in the input list, do the input-side
listeners get released? They do (the timeout callback aborts the
combined controller, which fires the auto-cleanup listener registered
on its signal, which calls the per-input removeEventListener), but
that path wasn't tested.

Adds a test that snapshots the source listener count before, asserts
it increased by 1 after combineAbortSignals returns, advances fake
timers past timeoutMs, and asserts the count returns to baseline.

Refs PR #4366.

* fix(test): use pathToFileURL for the warning-handler e2e import on Windows

CI failure on windows-latest:
  AssertionError: expected '\r\nnode:internal/modules/run_main:12…'
                  to match /DeprecationWarning.*Plain deprecation/
  Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in:
    file, data, and node are supported by the default ESM loader. On
    Windows, absolute paths must be valid file:// URLs. Received protocol 'd:'

The e2e test wrote a child script with an `import "<helperPath>"` where
helperPath was a raw Windows absolute path (`D:\a\qwen-code\...`). Node's
ESM loader parses that as a URL on Windows and rejects the `D:` "scheme".

Converted the helper path to a `file://` URL via `pathToFileURL`. macOS
test still passes; the Windows-specific schemes-must-be-URL behavior is
now honored. Refs PR #4366.

* fix(core,cli): address PR #4366 review batch — onAbort leak, migrate missed sites, tighten tests

Adopted 6 of the 7 review threads (skipping the debug-logging suggestion).

1. processFunctionCalls onAbort leak (CRITICAL): the new
   `finally { roundAbortController.abort(); }` in _runReasoningLoopInner
   would fire the `onAbort` handler in `processFunctionCalls` if
   scheduler.schedule or batchDone threw (the explicit
   removeEventListener at the old happy-path exit would be skipped),
   emitting spurious "Tool call cancelled by user abort." TOOL_RESULT
   events for every un-emitted callId — corrupting the transcript and
   misleading the model on the next round. Fixed by wrapping schedule
   + batchDone in their own try/finally so removeEventListener always
   runs before the outer finally's abort.

2. Migrate 3 new-from-main `new AbortController()` sites that this
   PR's audit missed (they came in via the merge from main):
   - goals/goalHook.ts (2 sites: judgeController, fallback signal) —
     consistency
   - hooks/promptHookRunner.ts (1 site: internalAbortController) —
     real leak (manual addEventListener without {once:true} or
     cleanup, exactly the pattern this PR exists to fix). Switched to
     createChildAbortController + finally `internalAbortController.abort()`
     for reverse cleanup on the success path.

3. Repro script (`listener-accumulation-repro.mjs`): inlined helper
   diverged from production — used WeakRef on child, while production
   was changed to strong-ref earlier in this PR. Updated the inlined
   copy to match production exactly, with a comment noting the
   intentional WeakRef-on-parent-only pattern.

4. warningHandler.ts: documented the snapshot-and-replace trade-offs
   in the JSDoc (late-added listeners bypass our filter; late
   `removeListener` calls have no effect on our fan-out). Tried the
   re-snapshot-per-warning approach the reviewer suggested but it
   doesn't work — `removeAllListeners('warning')` permanently removes
   the snapshot from Node's tracking, so a `process.listeners('warning')`
   filter at fan-out time always returns empty for prior listeners.
   The current design is the right trade-off; documentation is the
   correct fix.

5. abortController.test.ts: added three coverage gaps the reviewer
   identified —
   - createChildAbortController forwards custom maxListeners
   - manual cleanup() before scheduled timeout fires cancels it
   - timeoutMs <= 0 is treated as "no timeout"

6. Migrated `httpHookRunner.ts:202` (the lone caller of the deprecated
   `createCombinedAbortSignal`) to `combineAbortSignals` directly,
   then deleted `combinedAbortSignal.ts` + its test. All semantics
   covered by `combineAbortSignals` tests in abortController.test.ts.

Refreshed `migration-completeness.txt` (now empty — clean grep).
Tests: 194 pass across abortController/warningHandler/agent-runtime/
followup/hooks/goal/promptHook suites. Typecheck + prettier clean.

* docs(verification): commit the headless-scenario scripts referenced by the PR body

The PR body's "End-to-end scenarios I drove locally" section points at
docs/verification/abort-controller-refactor/scripts/02-lite.sh and 06-headless-sigint.sh.
These are the actual reproducible commands behind the EXIT codes /
warning counts reported there — checking them in so anyone can replay
without copy-pasting from the PR description.

Refs PR #4366.

* docs(verification): sync automated-results with current state

Two doc fixes the reviewer flagged:

- migration-completeness.txt was a 0-byte file with a confusing
  cross-reference. Populated with the actual grep command + its
  "(no output)" result so the empty-output state is explicit.

- automated-results.md still referenced combinedAbortSignal.test.ts (8
  tests, @deprecated shim) — both files were deleted in 94e8c5812 when
  httpHookRunner.ts migrated to combineAbortSignals directly. Replaced
  the line with a reference to httpHookRunner.test.ts. Also updated
  the test counts to reflect current state (26 abortController, 13
  warningHandler — both grew with the review cycle) and removed the
  stale combinedAbortSignal.ts entry from the prettier-check command.

Refs PR #4366.

* test(core): pin two abort-cascade behaviors PR #4366 introduced

Adopting 2 of 3 new review threads (the third — automated-results.md
drift — was already fixed in 5aa7110e4).

1. packages/core/src/agents/arena/ArenaManager.test.ts: pin the
   master→agent abort cascade introduced by switching per-agent
   controllers to `createChildAbortController(this.masterAbortController)`.
   New test spawns ≥2 agents, calls `manager.cancel()`, and asserts every
   `agentState.abortController.signal.aborted === true`. Existing cancel
   test only checked backend + status; if a future refactor re-introduced
   independent controllers, the cascade would silently regress.

2. packages/core/src/followup/speculation.test.ts: cover the
   `startSpeculation` abort wiring introduced when the manual
   addEventListener + .finally removeEventListener pattern got replaced
   by createChildAbortController + .finally abort(). Three tests:
   - parent abort propagates to state.abortController (lifetime contract)
   - parent-already-aborted fast path returns aborted state
   - parent-signal listener count returns to baseline after the fire-and-
     forget loop settles (reverse-cleanup proof)
   Mocked `runWithForkedChatModel` and `OverlayFs` so the background
   loop is a no-op — these tests only assert the synchronous wiring,
   not the loop's content.

* fix(test): speculation.test.ts TS errors + sync verification doc counts

Two real CI blockers in the just-added speculation tests (TS2554 and
TS2339) plus stale doc counts the reviewer flagged.

1. saveCacheSafeParams takes 3 positional args (generationConfig,
   history, model), not a single object. Compile error on every
   platform. Fixed by switching to the correct shape; also moved
   getEventListeners to a static `import` at the top of the file
   (dynamic `await import('node:events')` exposes EventEmitter's
   static method via the namespace type rather than as a direct
   property, so destructuring fails type-check).

2. docs/verification/abort-controller-refactor/README.md still claimed
   "18 + 1 GC" tests for abortController and "9" for warningHandler;
   actual current counts are 26 and 13. Also dropped the stale
   combinedAbortSignal reference and added a note about the new
   ArenaManager cascade + startSpeculation wiring pin tests.

Refreshed smoke-boot.log against current built bin (still 0.15.11,
which is what package.json reports on this branch).

Refs PR #4366.

* refactor(core): narrow PR #4366 scope per yiliang's review — revert independent-controller migrations

Adopting @yiliang114's review feedback (#4366 review comment, 2026-05-22):
keep only the migrations that fix the real leak path (the agent-runtime
parent→child chain that accumulates listeners on a long-lived parent
signal in long sessions) and revert the consistency-only migrations on
independent short-lived controllers.

Issue #4423 confirms the user-visible bug is the nested-chain
accumulation — the reverted sites do not contribute to that bug.

Migrations KEPT:
- agents/runtime/agent-interactive.ts (master + per-message round)
- agents/runtime/agent-core.ts (per-iteration + wait + processFunctionCalls)
- agents/runtime/agent-headless.ts (external → execution)
- hooks/promptHookRunner.ts (real cleanup leak: addEventListener without
  {once:true}, never removed)
- hooks/httpHookRunner.ts → combineAbortSignals direct (shim deleted)
- hookRunner.ts / functionHookRunner.ts / message-bus.ts: {once:true} only
- openaiContentGenerator/pipeline.ts band-aid removal (per-request signals
  are children of the per-round controller, which carries maxListeners=50)
- warningHandler.ts belt-and-suspenders

Migrations REVERTED (independent short-lived controllers; restored to
`new AbortController()` + their original cleanup patterns):
- agents/arena/ArenaManager.ts (master + per-agent)
- agents/background-agent-resume.ts (3 sites)
- core/client.ts (recall — restored manual addEventListener + finally
  removeEventListener pattern from main)
- followup/speculation.ts (restored parentAbortHandler + finally
  removeEventListener)
- goals/goalHook.ts (judgeController + fallback signal)
- memory/manager.ts (dream controller)
- services/chatCompressionService.ts (fallback signal)
- services/chatRecordingService.ts (autoTitle controller)
- tools/agent/agent.ts (fg + bg subagent controllers — restored manual
  onParentAbort + finally removeEventListener)
- tools/monitor.ts (entryAc)
- tools/shell.ts (promote + 3 entryAc)
- utils/fetch.ts (fetchWithTimeout)

Tests removed alongside the reverts:
- ArenaManager.test.ts "cancels cascades..." — the cascade itself was an
  intentional behavioral improvement that's now reverted, so the
  pin-test belongs with it
- speculation.test.ts "startSpeculation — abort-controller wiring" block
  (3 tests) — they tested helper-wired behavior we reverted

Verification docs updated to reflect the narrower scope.
Net change: 19 raw `new AbortController()` remain (intentional, per
migration-completeness.txt rationale); previously was 0.

Refs PR #4366, issue #4423.
2026-05-26 14:21:49 +08:00
.github ci: split Aliyun OSS sync into a separate post-release workflow (#4492) 2026-05-25 19:34:21 +08:00
.husky Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
.qwen docs(agents,pr-template): add Working Principles and restructure PR template (#4496) 2026-05-25 19:15:35 +08:00
.vscode Merge branch 'main' into feat/sandbox-config-improvements 2026-03-06 14:38:39 +08:00
docs fix(core): stop AbortSignal listener leak in long sessions (MaxListenersExceededWarning) (#4366) 2026-05-26 14:21:49 +08:00
docs-site feat: update docs 2025-12-15 09:47:03 +08:00
eslint-rules pre-release commit 2025-07-22 23:26:01 +08:00
integration-tests feat(core): add NotebookEdit tool for Jupyter notebooks 2026-05-21 00:06:15 +08:00
packages fix(core): stop AbortSignal listener leak in long sessions (MaxListenersExceededWarning) (#4366) 2026-05-26 14:21:49 +08:00
scripts ci: split Aliyun OSS sync into a separate post-release workflow (#4492) 2026-05-25 19:34:21 +08: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 feat(cli): do not append trailing space for directory completions (#4092) (#4288) 2026-05-23 23:37:23 +08: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 Merge branch 'main' into feat/add-vscode-settings-json-schema 2026-03-03 11:21:57 +08:00
.prettierrc.json pre-release commit 2025-07-22 23:26:01 +08:00
.yamllint.yml Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
AGENTS.md docs(agents,pr-template): add Working Principles and restructure PR template (#4496) 2026-05-25 19:15:35 +08:00
CONTRIBUTING.md chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
Dockerfile chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
esbuild.config.js fix(build): tree-shake React reconciler dev build to prevent PerformanceMeasure leak (#4462) 2026-05-23 21:00:32 +08:00
eslint.config.js fix(core): stop AbortSignal listener leak in long sessions (MaxListenersExceededWarning) (#4366) 2026-05-26 14:21:49 +08: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 feat(telemetry): client-side HTTP span + opt-in W3C traceparent propagation (#4384) (#4390) 2026-05-25 22:16:54 +08:00
package.json chore(release): v0.16.1 [skip ci] 2026-05-23 23:09:48 +08:00
README.md feat(cli,sdk): qwen serve daemon (Stage 1) (#3889) 2026-05-13 14:47:47 +08: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 test(channels): add comprehensive test suites for channel adapters 2026-03-27 15:26:39 +00:00

npm version License Node.js Version Downloads

QwenLM%2Fqwen-code | Trendshift

An open-source AI agent that lives in your terminal.

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

🎉 News

  • 2026-04-15: Qwen OAuth free tier has been discontinued. To continue using Qwen Code, switch to Alibaba Cloud Coding Plan, OpenRouter, Fireworks AI, or bring your own API key. Run qwen auth to configure.

  • 2026-04-13: Qwen OAuth free tier policy update: daily quota adjusted to 100 requests/day (from 1,000).

  • 2026-04-02: Qwen3.6-Plus is now live! Get an API key from Alibaba Cloud ModelStudio to access it through the OpenAI-compatible API.

  • 2026-02-16: Qwen3.5-Plus is now live!

Why Qwen Code?

Qwen Code is an open-source AI agent for the terminal, optimized for Qwen series models. It helps you understand large codebases, automate tedious work, and ship faster.

  • Multi-protocol, flexible providers: use OpenAI / Anthropic / Gemini-compatible APIs, Alibaba Cloud Coding Plan, OpenRouter, Fireworks AI, or bring your own API key.
  • Open-source, co-evolving: both the framework and the Qwen3-Coder model are open-source—and they ship and evolve together.
  • Agentic workflow, feature-rich: rich built-in tools (Skills, SubAgents) for a full agentic workflow and a Claude Code-like experience.
  • Terminal-first, IDE-friendly: built for developers who live in the command line, with optional integration for VS Code, Zed, and JetBrains IDEs.

Installation

Linux / macOS

bash -c "$(curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh)"

Windows (Run as Administrator)

Works in both Command Prompt and PowerShell:

powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')"

Note

: It's recommended to restart your terminal after installation to ensure environment variables take effect.

Manual Installation

Prerequisites

Make sure you have Node.js 22 or later installed. Download it from nodejs.org.

NPM

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

Homebrew (macOS, Linux)

brew install qwen-code

Quick Start

# Start Qwen Code (interactive)
qwen

# Then, in the session:
/help
/auth

On first use, you'll be prompted to sign in. You can run /auth anytime to switch authentication methods.

Example prompts:

What does this project do?
Explain the codebase structure.
Help me refactor this function.
Generate unit tests for this module.
Click to watch a demo video

🦞 Use Qwen Code for Coding Tasks in Claw

Copy the prompt below and paste it 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.

Authentication

Qwen Code supports the following authentication methods:

  • API Key (recommended): use an API key from Alibaba Cloud Model Studio (Beijing / intl) or any supported provider (OpenAI, Anthropic, Google GenAI, and other compatible endpoints).
  • Coding Plan: subscribe to the Alibaba Cloud Coding Plan (Beijing / intl) for a fixed monthly fee with higher quotas.

⚠️ Qwen OAuth was discontinued on April 15, 2026. If you were previously using Qwen OAuth, please switch to one of the methods above. Run qwen and then /auth to reconfigure.

Use an API key to connect to Alibaba Cloud Model Studio or any supported provider. Supports multiple protocols:

  • OpenAI-compatible: Alibaba Cloud ModelStudio, ModelScope, OpenAI, OpenRouter, and other OpenAI-compatible providers
  • Anthropic: Claude models
  • Google GenAI: Gemini models

The recommended way to configure models and providers is by editing ~/.qwen/settings.json (create it if it doesn't exist). This file lets you define all available models, API keys, and default settings in one place.

Quick Setup in 3 Steps

Step 1: Create or edit ~/.qwen/settings.json

Here is a complete example:

{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.6-plus",
        "name": "qwen3.6-plus",
        "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "description": "Qwen3-Coder via Dashscope",
        "envKey": "DASHSCOPE_API_KEY"
      }
    ]
  },
  "env": {
    "DASHSCOPE_API_KEY": "sk-xxxxxxxxxxxxx"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3.6-plus"
  }
}

Step 2: Understand each field

Field What it does
modelProviders Declares which models are available and how to connect to them. Keys like openai, anthropic, gemini represent the API protocol.
modelProviders[].id The model ID sent to the API (e.g. qwen3.6-plus, gpt-4o).
modelProviders[].envKey The name of the environment variable that holds your API key.
modelProviders[].baseUrl The API endpoint URL (required for non-default endpoints).
env A fallback place to store API keys (lowest priority; prefer .env files or export for sensitive keys).
security.auth.selectedType The protocol to use on startup (openai, anthropic, gemini, vertex-ai).
model.name The default model to use when Qwen Code starts.

Step 3: Start Qwen Code — your configuration takes effect automatically:

qwen

Use the /model command at any time to switch between all configured models.

More Examples
Coding Plan (Alibaba Cloud ModelStudio) — fixed monthly fee, higher quotas
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.6-plus",
        "name": "qwen3.6-plus (Coding Plan)",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
        "description": "qwen3.6-plus from ModelStudio Coding Plan",
        "envKey": "BAILIAN_CODING_PLAN_API_KEY"
      },
      {
        "id": "qwen3.5-plus",
        "name": "qwen3.5-plus (Coding Plan)",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
        "description": "qwen3.5-plus with thinking enabled from ModelStudio Coding Plan",
        "envKey": "BAILIAN_CODING_PLAN_API_KEY",
        "generationConfig": {
          "extra_body": {
            "enable_thinking": true
          }
        }
      },
      {
        "id": "glm-4.7",
        "name": "glm-4.7 (Coding Plan)",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
        "description": "glm-4.7 with thinking enabled from ModelStudio Coding Plan",
        "envKey": "BAILIAN_CODING_PLAN_API_KEY",
        "generationConfig": {
          "extra_body": {
            "enable_thinking": true
          }
        }
      },
      {
        "id": "kimi-k2.5",
        "name": "kimi-k2.5 (Coding Plan)",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
        "description": "kimi-k2.5 with thinking enabled from ModelStudio Coding Plan",
        "envKey": "BAILIAN_CODING_PLAN_API_KEY",
        "generationConfig": {
          "extra_body": {
            "enable_thinking": true
          }
        }
      }
    ]
  },
  "env": {
    "BAILIAN_CODING_PLAN_API_KEY": "sk-xxxxxxxxxxxxx"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3.6-plus"
  }
}

Subscribe to the Coding Plan and get your API key at Alibaba Cloud ModelStudio(Beijing) or Alibaba Cloud ModelStudio(intl).

Multiple providers (OpenAI + Anthropic + Gemini)
{
  "modelProviders": {
    "openai": [
      {
        "id": "gpt-4o",
        "name": "GPT-4o",
        "envKey": "OPENAI_API_KEY",
        "baseUrl": "https://api.openai.com/v1"
      }
    ],
    "anthropic": [
      {
        "id": "claude-sonnet-4-20250514",
        "name": "Claude Sonnet 4",
        "envKey": "ANTHROPIC_API_KEY"
      }
    ],
    "gemini": [
      {
        "id": "gemini-2.5-pro",
        "name": "Gemini 2.5 Pro",
        "envKey": "GEMINI_API_KEY"
      }
    ]
  },
  "env": {
    "OPENAI_API_KEY": "sk-xxxxxxxxxxxxx",
    "ANTHROPIC_API_KEY": "sk-ant-xxxxxxxxxxxxx",
    "GEMINI_API_KEY": "AIzaxxxxxxxxxxxxx"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "gpt-4o"
  }
}
Enable thinking mode (for supported models like qwen3.5-plus)
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.5-plus",
        "name": "qwen3.5-plus (thinking)",
        "envKey": "DASHSCOPE_API_KEY",
        "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "generationConfig": {
          "extra_body": {
            "enable_thinking": true
          }
        }
      }
    ]
  },
  "env": {
    "DASHSCOPE_API_KEY": "sk-xxxxxxxxxxxxx"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3.5-plus"
  }
}

Tip: You can also set API keys via export in your shell or .env files, which take higher priority than settings.jsonenv. See the authentication guide for full details.

Security note: Never commit API keys to version control. The ~/.qwen/settings.json file is in your home directory and should stay private.

Local Model Setup (Ollama / vLLM)

You can also run models locally — no API key or cloud account needed. This is not an authentication method; instead, configure your local model endpoint in ~/.qwen/settings.json using the modelProviders field.

Set generationConfig.contextWindowSize inside the matching provider entry and adjust it to the context length configured on your local server.

Ollama setup
  1. Install Ollama from ollama.com
  2. Pull a model: ollama pull qwen3:32b
  3. Configure ~/.qwen/settings.json:
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3:32b",
        "name": "Qwen3 32B (Ollama)",
        "baseUrl": "http://localhost:11434/v1",
        "description": "Qwen3 32B running locally via Ollama",
        "generationConfig": {
          "contextWindowSize": 131072
        }
      }
    ]
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3:32b"
  }
}
vLLM setup
  1. Install vLLM: pip install vllm
  2. Start the server: vllm serve Qwen/Qwen3-32B
  3. Configure ~/.qwen/settings.json:
{
  "modelProviders": {
    "openai": [
      {
        "id": "Qwen/Qwen3-32B",
        "name": "Qwen3 32B (vLLM)",
        "baseUrl": "http://localhost:8000/v1",
        "description": "Qwen3 32B running locally via vLLM",
        "generationConfig": {
          "contextWindowSize": 131072
        }
      }
    ]
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "Qwen/Qwen3-32B"
  }
}

Usage

As an open-source terminal agent, you can use Qwen Code in five primary ways:

  1. Interactive mode (terminal UI)
  2. Headless mode (scripts, CI)
  3. IDE integration (VS Code, Zed)
  4. SDKs (TypeScript, Python, Java)
  5. Daemon mode — qwen serve exposes ACP over HTTP+SSE so multiple clients share one agent (experimental)

Interactive mode

cd your-project/
qwen

Run qwen in your project folder to launch the interactive terminal UI. Use @ to reference local files (for example @src/main.ts).

Headless mode

cd your-project/
qwen -p "your question"

Use -p to run Qwen Code without the interactive UI—ideal for scripts, automation, and CI/CD. Learn more: Headless mode.

IDE integration

Use Qwen Code inside your editor (VS Code, Zed, and JetBrains IDEs):

Daemon mode (qwen serve, experimental)

cd your-project/
qwen serve
# → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge)

Run Qwen Code as a local HTTP daemon so IDE plugins, web UIs, CI scripts and custom CLIs all share one agent session over HTTP+SSE — instead of each spawning their own subprocess. Loopback bind has no auth by default (set QWEN_SERVER_TOKEN to enable bearer auth even on loopback); remote binds (--hostname 0.0.0.0) require a token — boot refuses without one. See:

SDKs

Build on top of Qwen Code with the available SDKs:

Python SDK example:

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())

Commands & Shortcuts

Session Commands

  • /help - Display available commands
  • /clear - Clear conversation history
  • /compress - Compress history to save tokens
  • /stats - Show current session information
  • /bug - Submit a bug report
  • /exit or /quit - Exit Qwen Code

Keyboard Shortcuts

  • Ctrl+C - Cancel current operation
  • Ctrl+D - Exit (on empty line)
  • Up/Down - Navigate command history

Learn more about Commands

Tip: In YOLO mode (--yolo), vision switching happens automatically without prompts when images are detected. Learn more about Approval Mode

Configuration

Qwen Code can be configured via settings.json, environment variables, and CLI flags.

File Scope Description
~/.qwen/settings.json User (global) Applies to all your Qwen Code sessions. Recommended for modelProviders and env.
.qwen/settings.json Project Applies only when running Qwen Code in this project. Overrides user settings.

The most commonly used top-level fields in settings.json:

Field Description
modelProviders Define available models per protocol (openai, anthropic, gemini, vertex-ai).
env Fallback environment variables (e.g. API keys). Lower priority than shell export and .env files.
security.auth.selectedType The protocol to use on startup (e.g. openai).
model.name The default model to use when Qwen Code starts.

See the Authentication section above for complete settings.json examples, and the settings reference for all available options.

Benchmark Results

Terminal-Bench Performance

Agent Model Accuracy
Qwen Code Qwen3-Coder-480A35 37.5%
Qwen Code Qwen3-Coder-30BA3B 31.3%

Ecosystem

Looking for a graphical interface?

  • 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

Troubleshooting

If you encounter issues, check the troubleshooting guide.

Common issues:

  • Qwen OAuth free tier was discontinued on 2026-04-15: Qwen OAuth is no longer available. Run qwen/auth and switch to API Key or Coding Plan. See the Authentication section above for setup instructions.

To report a bug from within the CLI, run /bug and include a short title and repro steps.

Connect with Us

Acknowledgments

This project is based on Google Gemini CLI. We acknowledge and appreciate the excellent work of the Gemini CLI team. Our main contribution focuses on parser-level adaptations to better support Qwen-Coder models.