# Technical Documentation: QwenLM/qwen-code

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/QwenLM/qwen-code)

### Overview (README.md)

<div align="center">

[](https://www.npmjs.com/package/@qwen-code/qwen-code)
[](./LICENSE)
[](https://nodejs.org/)
[](https://www.npmjs.com/package/@qwen-code/qwen-code)

<a href="https://trendshift.io/repositories/15287" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15287" alt="QwenLM%2Fqwen-code | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>

**The open-source AI coding agent for your terminal, editor, desktop, browser, and chat.**

<a href="https://qwenlm.github.io/qwen-code-docs/zh/users/overview">中文</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/de/users/overview">Deutsch</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/fr/users/overview">français</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/ja/users/overview">日本語</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/ru/users/overview">Русский</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/pt-BR/users/overview">Português (Brasil)</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/ko/users/overview">한국어</a>

</div>



## 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, Web UI, SDKs, and chat integrations (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:**

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

**Windows:**

```powershell
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.

<details>
<summary>NPM / Homebrew</summary>

**NPM** (requires [Node.js 22+](https://nodejs.org/)):

```bash
npm install @qwen-code/qwen-code@latest
```

**Homebrew** (macOS / Linux):

```bash
brew install qwen-code
```

</details>

## Quick Start

Open a terminal in your project and start Qwen Code:

```bash
cd /path/to/your-project
qwen
```

Inside the session, run `/auth` to configure your provider and API key. Then try:

```text
Explain this repository and show me where to start.
```

See the [Authentication Guide](https://qwenlm.github.io/qwen-code-docs/en/users/configuration/auth/) and [Settings Reference](https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/) for detailed setup.

<details>
<summary>See the terminal interface</summary>



</details>

## How to Use Qwen Code

Choose the interface that fits your workflow:

| Interface    | Get started                                                                                                                                                                                                                                                                                                                                                                                               |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Terminal** | Run `qwen` in your project — see [Quick Start](#quick-start).                                                                                                                                                                                                                                                                                                                                             |
| **Desktop**  | [Download Qwen Code Desktop](https://github.com/QwenLM/qwen-code/releases/tag/desktop-latest) for macOS, Windows, and Linux.                                                                                                                                                                                                                                                                              |
| **Web**      | Run `qwen serve --open` to open the [Web UI](https://qwenlm.github.io/qwen-code-docs/en/users/qwen-serve/) in your browser. _(experimental)_                                                                                                                                                                                                                                                              |
| **Editor**   | Set up [VS Code](https://qwenlm.github.io/qwen-code-docs/en/users/integration-vscode/), [Zed](https://qwenlm.github.io/qwen-code-docs/en/users/integration-zed/), or [JetBrains](https://qwenlm.github.io/qwen-code-docs/en/users/integration-jetbrains/).                                                                                                                                                |
| **Chat**     | Connect [Telegram](https://qwenlm.github.io/qwen-code-docs/en/users/features/channels/telegram/), [DingTalk](https://qwenlm.github.io/qwen-code-docs/en/users/features/channels/dingtalk/), [WeChat](https://qwenlm.github.io/qwen-code-docs/en/users/features/channels/weixin/), or [Feishu](https://qwenlm.github.io/qwen-code-docs/en/users/features/channels/feishu/), then run `qwen channel start`. |

For automation and custom integrations:

- **Headless** — Run `qwen -p "..."` in scripts, CI/CD, or batch jobs.
- **SDKs** — Build with [TypeScript](./packages/sdk-typescript/README.md), [Python](./packages/sdk-python/README.md), or [Java](./packages/sdk-java/qwencode/README.md).
- **Daemon** — Run `qwen serve` to connect clients over HTTP + SSE (ACP). _(experimental)_ [Daemon guide](https://qwenlm.github.io/qwen-code-docs/en/users/qwen-serve/).
- **Computer Use** — Set up [native MCP tools](./packages/cua-driver/README.md#agent-integration) or [Node REPL + SDK](./packages/node-repl/README.md#usage) in Qwen Code, Claude Code, or Codex.

<details>
<summary>SDK example (Python)</summary>

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

</details>

## 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](https://github.com/wenshao/codeagents/blob/main/docs/comparison/qwen-code-improvement-report.md), 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)                |     ✓     |      —      |

## Qwen Code Evaluation

On **SWE-bench Verified**, seven Qwen Code versions scored **76.40%–77.80%** on average using the same Qwen 3.7 Max model (500 tasks, three trials per version).



[Full results and evaluation configuration →](./docs/evaluation.md)

## Ecosystem

- [**Qwen Code Desktop**](https://github.com/QwenLM/qwen-code/releases/tag/desktop-latest) — Official desktop app for macOS, Windows, and Linux
- [**AionUi**](https://github.com/iOfficeAI/AionUi) — A modern GUI for command-line AI tools including Qwen Code
- [**Gemini CLI Desktop**](https://github.com/Piebald-AI/gemini-cli-desktop) — A cross-platform desktop/web/mobile UI for Qwen Code
- [**qwen-audio-agent**](https://github.com/QwenAudio/qwen-audio-agent) — Realtime full-duplex voice frontend for Qwen Code over ACP — talk hands-free with barge-in and a local wake word

- [**🦞 Qwen Code Claw**](https://github.com/openclaw/acpx) — Let other agents (Claude, Codex, etc.) delegate coding tasks to Qwen Code via ACP. Paste this prompt into your agent:

```text
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**](https://github.com/modelstudioai/cli) — Official CLI for Aliyun's AI platform (`bailian-cli`). Extends Qwen Code with image/video generation, knowledge retrieval, app orchestration, and model deployment

## Privacy

See the [Terms of Service and Privacy Notice](./docs/users/support/tos-privacy.md), including [Chrome extension and Browser Use data handling](./docs/users/support/tos-privacy.md#chrome-extension-and-browser-use).

## Contributing

Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.

## Acknowledgments

This project was originally based on [Google Gemini CLI](https://github.com/google-gemini/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.

---

### Index (`docs/index.md`)

# Qwen Code Documentation

Welcome to the Qwen Code documentation. Qwen Code is an agentic coding tool that lives in your terminal and helps you turn ideas into code faster than ever before.

## Documentation Sections

### [User Guide](./users/overview)

Learn how to use Qwen Code as an end user. This section covers:

- Basic installation and setup
- Common usage patterns
- Features and capabilities
- Configuration options
- Troubleshooting

### [Developer Guide](./developers/architecture)

Learn how to contribute to and develop Qwen Code. This section covers:

- Architecture overview
- Contributing guidelines
- Core concepts and implementation details
- Tools and development workflow
- Extension and plugin development

---

### Users Overview (`docs/users/overview.md`)

# Qwen Code overview

[](https://npm-compare.com/@qwen-code/qwen-code)
[](https://www.npmjs.com/package/@qwen-code/qwen-code)

> Learn about Qwen Code, Qwen's agentic coding tool that lives in your terminal and helps you turn ideas into code faster than ever before.

## Get started in 30 seconds

### Install Qwen Code:

The recommended installer uses a standalone archive when one is available for
your platform. If it falls back to npm, Node.js 22 or later with npm must be
available on PATH.

**Linux / macOS**

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

**Windows**

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

> [!note]
>
> It's recommended to restart your terminal after installation if `qwen` is not
> immediately available on PATH. If the installation fails, please refer to
> [Manual Installation](./quickstart#manual-installation) in the Quickstart
> guide. For offline installation, download a release archive and run the
> installer with `--archive PATH`; keep `SHA256SUMS` next to the archive.

### Start using Qwen Code:

```bash
cd your-project
qwen
```

On first launch you'll be prompted to connect a model provider. The menu offers **QwenCloud** (the official recommended setup), **Alibaba ModelStudio**, **Third-party Providers** (built-in providers such as DeepSeek, MiniMax, Z.AI, Kimi, and OpenRouter, connected with an API key), and **Custom Provider** (a local server, proxy, or unsupported provider). For a QwenCloud key, choose **QwenCloud → Token Plan** or **QwenCloud → Standard API Key** and then pick the platform (China or International) — see [Authentication](./configuration/auth). For the [Alibaba Cloud Token Plan](https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856) ([intl](https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model)), choose **Alibaba ModelStudio → Token Plan**; to use a ModelStudio API key, choose **Alibaba ModelStudio → Standard API Key** and follow the API setup guide ([Beijing](https://bailian.console.aliyun.com/cn-beijing/?tab=doc#/doc/?type=model&url=3023091) / [intl](https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model&url=2974721)). Then let's start with understanding your codebase. Try one of these commands:

```
what does this project do?
```



You'll be prompted to log in on first use. That's it! [Continue with Quickstart (5 mins) →](./quickstart)

> [!tip]
>
> See [troubleshooting](./support/troubleshooting) if you hit issues.

> [!note]
>
> **New VS Code Extension (Beta)**: Prefer a graphical interface? Our new **VS Code extension** provides an easy-to-use native IDE experience without requiring terminal familiarity. Simply install from the marketplace and start coding with Qwen Code directly in your sidebar. Download and install the [Qwen Code Companion](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion) now.

## What Qwen Code does for you

- **Build features from descriptions**: Tell Qwen Code what you want to build in plain language. It will make a plan, write the code, and ensure it works.
- **Debug and fix issues**: Describe a bug or paste an error message. Qwen Code will analyze your codebase, identify the problem, and implement a fix.
- **Navigate any codebase**: Ask anything about your team's codebase, and get a thoughtful answer back. Qwen Code maintains awareness of your entire project structure, can find up-to-date information from the web, and with [MCP](./features/mcp) can pull from external datasources like Google Drive, Figma, and Slack.
- **Automate tedious tasks**: Fix fiddly lint issues, resolve merge conflicts, and write release notes. Do all this in a single command from your developer machines, or automatically in CI.
- **[Followup suggestions](./features/followup-suggestions)**: Qwen Code predicts what you want to type next and shows it as ghost text. Press Tab to accept, or just keep typing to dismiss.

## Why developers love Qwen Code

- **Works in your terminal**: Not another chat window. Not another IDE. Qwen Code meets you where you already work, with the tools you already love.
- **Takes action**: Qwen Code can directly edit files, run commands, and create commits. Need more? [MCP](./features/mcp) lets Qwen Code read your design docs in Google Drive, update your tickets in Jira, or use _your_ custom developer tooling.
- **Unix philosophy**: Qwen Code is composable and scriptable. `tail -f app.log | qwen -p "Slack me if you see any anomalies appear in this log stream"` _works_. Your CI can run `qwen -p "If there are new text strings, translate them into French and raise a PR for @lang-fr-team to review"`.

---

### Users Quickstart (`docs/users/quickstart.md`)

# Quickstart

> 👏 Welcome to Qwen Code!

This quickstart guide will have you using AI-powered coding assistance in just a few minutes. By the end, you'll understand how to use Qwen Code for common development tasks.

## Before you begin

Make sure you have:

- A **terminal** or command prompt open
- A code project to work with
- An API key or Token Plan subscription from 千问AI平台 / QwenCloud ([China](https://platform.qianwenai.com/docs/developer-guides/administration/api-keys) / [International](https://docs.qwencloud.com/developer-guides/administration/api-keys)) — the recommended setup — or an API key from Alibaba Cloud ModelStudio ([Beijing](https://bailian.console.aliyun.com/) / [intl](https://modelstudio.console.alibabacloud.com/)), or an Alibaba Cloud Token Plan ([Beijing](https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856) / [intl](https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model)) subscription

## Step 1: Install Qwen Code

To install Qwen Code, use one of the following methods:

### Quick Install (Recommended)

**Linux / macOS**

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

**Windows**

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

> [!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](https://nodejs.org/en/download).

**NPM**

```bash
npm install @qwen-code/qwen-code@latest
```

**Homebrew (macOS, Linux)**

```bash
brew install qwen-code
```

## Step 2: Set up authentication

When you start an interactive session with the `qwen` command, you'll be prompted to configure authentication:

```bash
# You'll be prompted to set up authentication on first use
qwen
```

```bash
# Or run /auth anytime to change authentication method
/auth
```

The first-run menu lets you connect a model provider. Choose one of:

- **QwenCloud** — the recommended setup. Opens a sub-menu:
  - **Token Plan**: subscription with a monthly Credit quota on a dedicated endpoint (Personal and Team editions).
  - **Standard API Key**: pay-as-you-go, connect with an existing platform API key.

  Either way you then pick the platform explicitly (China or International), so the UI language never decides your endpoint. See [Authentication → Option 1: QwenCloud](./configuration/auth) for details.

- **Alibaba ModelStudio** — opens a sub-menu:
  - **Token Plan**: usage-based billing with a dedicated endpoint, aimed at teams and companies. See the [Token Plan guide](https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856) ([intl](https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model)) for setup instructions.
  - **Standard API Key**: connect with an existing API key from Alibaba Cloud ModelStudio ([Beijing](https://bailian.console.aliyun.com/) / [intl](https://modelstudio.console.alibabacloud.com/)). See the API setup guide ([Beijing](https://bailian.console.aliyun.com/cn-beijing/?tab=doc#/doc/?type=model&url=3023091) / [intl](https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model&url=2974721)) for details.
- **Third-party Providers** — choose a built-in provider (DeepSeek, Grok, MiniMax, Z.AI, Kimi, Idealab, ModelScope, OpenRouter, Requesty, and more) and connect with an API key.
- **Custom Provider** — manually connect a local server, proxy, or unsupported provider.

> ⚠️ **Note**: Qwen OAuth was discontinued on April 15, 2026. If you were previously using Qwen OAuth, please switch to one of the methods above.

> [!note]
>
> Qwen Code stores its configuration and customizations in `.qwen` directories: `~/.qwen/` for user-level settings (e.g. `settings.json`) and global custom commands, and `<project>/.qwen/` for project-specific files such as custom commands and summaries.

> [!tip]
>
> To configure authentication, start Qwen Code and run `/auth`. Use `/doctor` to check your current configuration at any time. See the [Authentication](./configuration/auth) page for details.

## Step 3: Start your first session

Open your terminal in any project directory and start Qwen Code:

```bash
# optional
cd /path/to/your/project
# start qwen
qwen
```

You'll see the Qwen Code welcome screen with your session information, recent conversations, and latest updates. Type `/help` for available commands.

## Chat with Qwen Code

### Ask your first question

Qwen Code will analyze your files and provide a summary. You can also ask more specific questions:

```
explain the folder structure
```

You can also ask Qwen Code about its own capabilities:

```
what can Qwen Code do?
```

> [!note]
>
> Qwen Code reads your files as needed - you don't have to manually add context. Qwen Code also has access to its own documentation and can answer questions about its features and capabilities.

### Make your first code change

Now let's make Qwen Code do some actual coding. Try a simple task:

```
add a hello world function to the main file
```

Qwen Code will:

1. Find the appropriate file
2. Show you the proposed changes
3. Ask for your approval
4. Make the edit

> [!note]
>
> Qwen Code always asks for permission before modifying files. You can approve individual changes or enable "Accept all" mode for a session.

### Use Git with Qwen Code

Qwen Code makes Git operations conversational:

```
what files have I changed?
```

```
commit my changes with a descriptive message
```

You can also prompt for more complex Git operations:

```
create a new branch called feature/quickstart
```

```
show me the last 5 commits
```

```
help me resolve merge conflicts
```

### Fix a bug or add a feature

Qwen Code is proficient at debugging and feature implementation.

Describe what you want in natural language:

```
add input validation to the user registration form
```

Or fix existing issues:

```
there's a bug where users can submit empty forms - fix it
```

Qwen Code will:

- Locate the relevant code
- Understand the context
- Implement a solution
- Run tests if available

### Test out other common workflows

There are a number of ways to work with Qwen Code:

**Refactor code**

```
refactor the authentication module to use async/await instead of callbacks
```

**Write tests**

```
write unit tests for the calculator functions
```

**Update documentation**

```
update the README with installation instructions
```

**Code review**

```
review my changes and suggest improvements
```

> [!tip]
>
> **Remember**: Qwen Code is your AI pair programmer. Talk to it like you would a helpful colleague - describe what you want to achieve, and it will help you get there.

## Essential commands

Here are the most important commands for daily use:

| Command               | What it does                                     | Example                            |
| --------------------- | ------------------------------------------------ | ---------------------------------- |
| `qwen`                | start Qwen Code                                  | `qwen`                             |
| `/auth`               | Change authentication method (in session)        | `/auth`                            |
| `/doctor`             | Check current authentication and environment     | `/doctor`                          |
| `/help`               | Display help information for available commands  | `/help` or `/?`                    |
| `/compress`           | Replace chat history with summary to save Tokens | `/compress`                        |
| `/clear`              | Clear conversation history and free up context   | `/clear` (aliases: `reset`, `new`) |
| `/theme`              | Change Qwen Code visual theme                    | `/theme`                           |
| `/language`           | View or change language settings                 | `/language`                        |
| → `ui [language]`     | Set UI interface language                        | `/language ui zh-CN`               |
| → `output [language]` | Set LLM output language                          | `/language output Chinese`         |
| `/quit`               | Exit Qwen Code immediately                       | `/quit` or `/exit`                 |

See the [CLI reference](./features/commands) for a complete list of commands.

## Pro tips for beginners

**Be specific with your requests**

- Instead of: "fix the bug"
- Try: "fix the login bug where users see a blank screen after entering wrong credentials"

**Use step-by-step instructions**

- Break complex tasks into steps:

```
1. create a new database table for user profiles
2. create an API endpoint to get and update user profiles
3. build a webpage that allows users to see and edit their information
```

**Let Qwen Code explore first**

- Before making changes, let Qwen Code understand your code:

```
analyze the database schema
```

```
build a dashboard showing products that are most frequently returned by our UK customers
```

**Save time with shortcuts**

- Press `?` to see all available keyboard shortcuts
- Use Tab for command completion
- Press ↑ for command history
- Type `/` to see all slash commands

## Getting help

- **In Qwen Code**: Type `/help` or ask "how do I..."
- **Documentation**: You're here! Browse other guides
- **Community**: Join our [GitHub Discussion](https://github.com/QwenLM/qwen-code/discussions) for tips and support

---

### Users Features Channels Overview (`docs/users/features/channels/overview.md`)

# Channels

Channels let you interact with a Qwen Code agent from messaging platforms like Telegram, WeChat, QQ, DingTalk, WeCom, or Feishu, instead of the terminal. You send messages from your phone or desktop chat app, and the agent responds just like it would in the CLI.

Code-hosting platforms (starting with [GitHub](./github)) and authenticated workspace accounts (starting with [DingTalk Workspace](./dws)) are also supported through channels. [Email](./email) connects a dedicated mailbox through IMAP and SMTP.

## How It Works

When you run `qwen channel start`, Qwen Code:

1. Reads channel configurations from your `settings.json`
2. Spawns a single agent process using the [Agent Client Protocol (ACP)](../../../developers/architecture.md)
3. Connects to each messaging platform and starts listening for messages
4. Routes incoming messages to the agent and sends responses back to the correct chat

All channels share one agent process with isolated sessions per user. Each channel can have its own working directory, model, and instructions.

## Quick Start

1. Set up a bot or authenticated workspace account (see channel-specific guides: [Telegram](./telegram), [WeChat](./weixin), [QQ Bot](./qqbot), [DingTalk](./dingtalk), [DingTalk Workspace](./dws), [WeCom](./wecom), [WeCom Callback](./wecom-callback), [Feishu](./feishu), [GitHub](./github), [Email](./email))
2. Add the channel configuration to `~/.qwen/settings.json`
3. Run `qwen channel start` to start all channels, or `qwen channel start <name>` for a single channel

Want to connect a platform that isn't built in? See [Plugins](./plugins) to add a custom adapter as an extension.

## Configuration

Channels are configured under the `channels` key in `settings.json`. Each channel has a name and a set of options:

```json
{
  "channels": {
    "my-channel": {
      "type": "telegram",
      "token": "$MY_BOT_TOKEN",
      "privatePolicy": "allowlist",
      "allowedUsers": ["123456789"],
      "sessionScope": "user",
      "cwd": "/path/to/working/directory",
      "instructions": "Optional system instructions for the agent.",
      "groupPolicy": "disabled",
      "groups": {
        "*": { "requireMention": true }
      }
    }
  }
}
```

### Options

| Option              | Required         | Description                                                                                                                                                                                                 |
| ------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`              | Yes              | Channel type: `telegram`, `weixin`, `qq`, `dingtalk`, `dws`, `wecom`, `wecom-callback`, `feishu`, `github`, `gitlab`, `email`, or a custom type from an extension (see [Plugins](./plugins))                |
| `token`             | See description  | Required for Telegram (bot token) and WeCom Callback (verification token). Supports `$ENV_VAR` syntax. See [WeCom Callback](./wecom-callback) for application credentials                                   |
| `clientId`          | DingTalk, Feishu | DingTalk AppKey or Feishu App ID. Supports `$ENV_VAR` syntax                                                                                                                                                |
| `clientSecret`      | DingTalk, Feishu | DingTalk AppSecret or Feishu App Secret. Supports `$ENV_VAR` syntax                                                                                                                                         |
| `botId`             | WeCom            | WeCom intelligent robot Bot ID. Supports `$ENV_VAR` syntax. See [WeCom](./wecom)                                                                                                                            |
| `secret`            | WeCom            | WeCom intelligent robot Secret. Supports `$ENV_VAR` syntax. See [WeCom](./wecom)                                                                                                                            |
| `model`             | No               | Model to use for this channel (e.g., `qwen3.5-plus`). Overrides the default model. Useful for multimodal models that support image input                                                                    |
| `privatePolicy`     | No               | Private access: `disabled`, `allowlist`, `pairing`, or `open`; new managed channels default to `pairing`                                                                                                    |
| `allowedUsers`      | No               | Private user IDs allowed without pairing (used by private `allowlist` and `pairing` policies)                                                                                                               |
| `sessionScope`      | No               | How sessions are scoped: `user` (default), `chat_thread`, or `single`. Legacy `thread` remains compatible when already configured but is not offered for new Web Shell configurations                       |
| `sessionRotation`   | No               | Start a fresh session after `maxTurns` routed messages or `maxAgeHours` elapsed, whichever comes first. Requires a positive bound and cannot be combined with `multiSession`.                               |
| `multiSession`      | No               | Retain up to eight owner-scoped named tasks in one chat. Requires daemon-managed mode, `sessionScope: "user"`, no webhooks or group-history backfill, and no enabled Channel loops                          |
| `cwd`               | No               | Working directory for the agent. Defaults to the current directory                                                                                                                                          |
| `approvalMode`      | No               | Tool approval mode for channel sessions. Unattended webhook tasks require `yolo`; the setting applies to every session on the channel                                                                       |
| `instructions`      | No               | Custom instructions prepended to the first message of each session                                                                                                                                          |
| `webhooks`          | No               | Webhook sources and delivery targets for daemon-managed channels. See [Webhook-triggered tasks](#webhook-triggered-tasks)                                                                                   |
| `groupPolicy`       | No               | Group chat access: `disabled` (default), `allowlist`, `pairing`, or `open`. See [Group Chats](#group-chats)                                                                                                 |
| `senderPolicy`      | No               | Deprecated: `allowlist`, `pairing`, or `open`; used only when `privatePolicy` is absent and DM access is enabled                                                                                            |
| `dmPolicy`          | No               | Deprecated: `open` or `disabled`; used only when `privatePolicy` is absent                                                                                                                                  |
| `operators`         | No               | Who may operate a shared session (`/approve`, `/clear`, `/loop`, ...). Unset or empty grants no shared-session operator permissions. See [Shared-Session Operators](#shared-session-operators)              |
| `groupHistoryLimit` | No               | Opt-in group history backfill. `0` or omitted disables it. A positive number persists that many unmentioned group messages from senders admitted by the group member policy for the next bot mention/reply. |
| `groups`            | No               | Per-group settings. Keys are group chat IDs or `"*"` for defaults. See [Group Chats](#group-chats)                                                                                                          |
| `dispatchMode`      | No               | What happens when you send a message while the bot is busy: `steer` (default), `collect`, or `followup`. See [Dispatch Modes](#dispatch-modes)                                                              |

### Message Routes

Use `messageRoutes` to run multiple workflows through one Channel connection.
Each key is a case-sensitive message prefix and each value contains instructions
for that route:

```json
{
  "messageRoutes": {
    "/review": "Review the requested pull request.",
    "/QA": "Answer questions about this repository."
  },
  "defaultMessageRoute": "/QA"
}
```

A prefix must be followed by whitespace and a non-empty message. The longest
matching prefix wins; leading mentions are skipped when matching. The matched prefix is removed before the
message reaches the agent. Route instructions are combined with the Channel's
common `instructions` on the first turn of each session. Routes use separate
sessions within the configured `sessionScope`. Existing Channel memory remains
shared at its configured chat/thread scope; routes are not a permissions sandbox.

Without `defaultMessageRoute`, unmatched chat messages are ignored. To accept
ordinary messages, set it to a key in `messageRoutes`; those messages keep their
original text and use that route's instructions and session. Empty prefixes and
empty route maps are invalid. Route instructions may be empty. Prefixes and
instructions are trimmed when loading configuration. `messageRoutes` cannot be
combined with `multiSession`.

Routing does not grant access: private and group policies still apply. Shared
and agent commands also need a route prefix, for example `/QA /help`. Messages
without user-authored text and provider-generated system events remain outside
prefix routing.

### Private Policy

`privatePolicy` controls private conversations independently of groups:

- **`disabled`** — Ignore private messages without creating pairing requests.
- **`allowlist`** — Only users listed in the top-level `allowedUsers` may send private messages.
- **`pairing`** — Users in `allowedUsers` are admitted directly. Other users need a locally approved user pairing. See [DM Pairing](#dm-pairing).
- **`open`** — Anyone can send private messages.

New managed channels default to `pairing`. The deprecated `senderPolicy` and `dmPolicy` keys remain supported: an explicit `privatePolicy` takes precedence; otherwise `dmPolicy: "disabled"` disables private access, and otherwise `senderPolicy` supplies the policy (default `allowlist`). These old keys do not restrict group members. Saving from the editor uses `privatePolicy` and preserves existing deprecated keys.

### Shared-Session Operators

In a shared session (`sessionScope: "chat_thread"` or `"single"`, and group chats under `"thread"`), some commands affect everyone in the conversation: `/approve`, `/deny`, `/cancel`, `/clear`, `/who`, `/status`, `/loop`, `/btw`, the loop tool, and steering an in-flight turn. Only the session's operators may use them; other members' messages queue instead of steering. Sessions that are not shared belong to their own sender, who may always use them.

Set `operators` to the user IDs allowed to manage shared sessions. Omitted or empty `operators` grants nobody this role. Private allowlists, user pairing, group pairing, and group member lists never imply operator permissions. Operators must still pass normal conversation admission.

For example, `groups: { "*": { "senders": "open" } }` with `operators: ["alice"]` lets every admitted group member start a turn, but only Alice can manage its shared session.

A saved loop is checked against the same rule when it fires, using its creator as the sender, and is disabled if the creator is no longer an operator.

### Session Scope

Controls how conversation sessions are managed:

- **`user`** (default) — One session per user. All messages from the same user share a conversation.
- **`chat_thread`** — One session per chat thread/topic, shared by participants in that thread.
- **`thread`** — Legacy thread/topic routing retained for existing configurations.
- **`single`** — One shared session for all users. Everyone shares the same conversation.

### Named Tasks

Daemon-managed Channels can retain several named conversations for the same user in one chat:

```json
{
  "channels": {
    "my-channel": {
      "type": "telegram",
      "sessionScope": "user",
      "multiSession": true
    }
  }
}
```

The catalog is private to the exact channel, chat, and sender. Task names use 1–32 ASCII letters, numbers, underscores, or hyphens, and are unique case-insensitively. Up to eight tasks may be open; closing a task detaches it without deleting its transcript, so selecting it later reopens the exact conversation. Session IDs are never accepted by or shown in chat commands.

Named results identify their originating task: direct chats use `[task]`, while group chats use `[sender · task]`. Named text permission prompts also show the exact request ID and the corresponding `/approve <id>`, `/approve-always <id>`, and `/deny <id>` commands. The label is presentation-only and is not stored in the model transcript.

One task remains selected to receive the next normal message, but other named tasks may keep running concurrently. `/session new <name>` shares the configured workspace, while `/session new <name> --worktree` creates an isolated checkout for that task under the daemon workspace's `.qwen/worktrees/` directory. The daemon verifies the persisted worktree owner before reopening the task after a restart; a missing, changed, or foreign ownership record fails closed instead of silently moving the task into the shared workspace. Creating or selecting another task does not cancel or retarget earlier work, and late results retain their originating task label. A busy task cannot be closed, but its active prompt can be cancelled with `/session cancel [<name>]` through the existing Channel cancellation behavior. Independently queued turns are not cancelled, but in `collect` dispatch mode any follow-ups buffered behind the cancelled prompt are discarded by that existing behavior. Media preparation is not targeted. Bare permission commands apply only to the selected task, while an explicit request ID can answer an owned inactive task. `/clear`, `/new`, and `/reset` also work on a selected worktree task: the task gets a fresh conversation while its worktree and files are kept. A busy worktree task refuses the reset until its prompt finishes, and a task whose worktree record was damaged reports the failure without touching files. Channel memory remains scoped to the chat rather than to a named task.

This mode is unavailable in standalone `qwen channel start`, with webhooks, with non-zero channel or group `groupHistoryLimit`, or with Channel loops. If an enabled loop already exists for that channel, the daemon worker refuses to start until the loop is disabled.

### Session Rotation

Long-lived routes can start a fresh conversation after a configured limit:

```json
{
  "channels": {
    "my-bot": {
      "type": "dingtalk",
      "sessionScope": "chat_thread",
      "sessionRotation": { "maxTurns": 200, "maxAgeHours": 24 }
    }
  }
}
```

Either positive bound may be used alone; the first one reached triggers rotation before the next routed message. `maxTurns` counts messages routed to the session, including messages handled without a model turn. The count and age start persist across restarts. A route with a running or queued message waits for a later idle message to rotate, so steady traffic may pass the configured bound. Rotation clears that route's conversation context, retires its old session, and posts a notice in the triggering chat or thread. With `sessionScope: "single"`, other chats sharing the session do not receive the notice. Omitting `sessionRotation` preserves ongoing session reuse. Named tasks (`multiSession`) cannot use this option.

### Channel Memory

Channel memory stores durable context for one chat or thread. Entries have stable
IDs, so a list response can be used for deterministic follow-up operations.

- `记住：默认使用 staging 环境` is the deterministic form and saves exactly one
  scalar entry for the current chat or thread.
- To save several separate facts in one request, use a natural phrase routed
  through the classifier. For example:
  `请记住这三条约定：使用 staging；发布前测试；优先中文回复` creates entries
  that you can manage independently. Exact duplicate facts are skipped and
  reported without creating another entry. Requests containing credential-like
  text are rejected; remove secrets and save the non-sensitive facts separately.
- `查看记忆` lists entries and their stable IDs. Use `查看第 2 页记忆` to view
  a later page, `查看记忆 <id>` to view one entry, or a natural filtered
  request such as `只看中文偏好` to list the matching entries.
- `查看刚才那条记忆`, `把关于 staging 的记忆改成默认使用 production`, and
  `忘掉刚才那条` work when the natural reference resolves to exactly one entry.
  Natural updates and removals first show the proposed change. Confirm an
  update with `确认更新记忆` or `confirm memory update`, or a removal with
  `确认删除记忆` or `confirm memory removal`, within 60 seconds. Exact-ID
  updates and removals remain immediate and do not need confirmation.
- `清空记忆` starts the clear-all confirmation flow; `确认清空记忆` completes
  it.

When a natural inspect, update, or removal request matches multiple entries,
the bot returns the candidate IDs and previews without changing memory. There
is no pending selection for an ambiguous result: retry the request with one
exact ID, such as `忘掉 m-a31f0d82c7e4`. Exact-ID operations remain the
deterministic fast path. A natural request with no match reports that no entry
matched.

Pending update, removal, and clear confirmations apply only to the sender and
chat or thread that created them. A newer clear, natural update, or natural
removal proposal replaces an older pending one for that sender and target.
Pending confirmations are discarded when the channel process restarts.

The legacy slash aliases `/remember-channel`, `/channel-memory`, and
`/forget-channel` have been removed. They are no longer channel-memory
commands.

Channel memory follows the channel access gates. Any message accepted by
private access, group admission, group member settings,
pairing, and mention requirements can read, write, update, or clear memory for
that chat or thread. Accepted members of the same group share that group's
target store. Use a group's `senders: "allowlist"` with its `allowedUsers` when group memory should be
limited to trusted senders — under `senders: "open"` every member of an
admitted group shares that group's memory store.

Existing legacy `CHANNEL.md` memory is migrated automatically to structured
`CHANNEL.json` storage on the first mutation. Structured memory persists across
standalone channel and daemon-managed channel restarts, and is injected when a
fresh target-scoped session starts, including after `/clear`.

After that initial injection, each accepted message also recalls up to three
relevant entries for that message. This keeps durable facts available during a
long-running session without adding every stored entry to every turn. Recall is
based on the current message and does not modify the stored memory.

Memory remains keyed to the current chat or thread. It is not injected or
recalled in a `sessionScope: single` session, because that session is shared
across the whole channel rather than scoped to one target.

Channel memory does not automatically learn facts from normal conversation or
accept `第一个` as confirmation for an ambiguous natural reference. Use a clear
remember request and an exact entry ID when a natural reference is ambiguous.

### Token Security

Bot tokens should not be stored directly in `settings.json`. Instead, use environment variable references:

```json
{
  "token": "$TELEGRAM_BOT_TOKEN"
}
```

Set the actual token in your shell environment or in a `.env` file that gets loaded before running the channel.

## DM Pairing

When `privatePolicy` is set to `"pairing"`, unknown senders go through an approval flow:

1. An unknown user sends a message to the bot
2. The bot replies with an 8-character pairing code (e.g., `VEQDDWXJ`)
3. The user shares the code with you (the bot operator)
4. You approve them via CLI:

```bash
qwen channel pairing approve my-channel VEQDDWXJ
```

Once approved, the user's ID is saved to the channel's workspace-scoped allowlist (`~/.qwen/channels/<workspace-scope>/<name>-allowlist.json`) and all future messages go through normally. Pairing state is scoped per workspace, so two workspaces using the same channel name keep separate approvals.

### Pairing CLI Commands

```bash
# List pending pairing requests
qwen channel pairing list my-channel

# Approve a request by code
qwen channel pairing approve my-channel <CODE>
```

Run these from the channel's workspace directory (or pass `--cwd <dir>`) — pairing state is stored per workspace.

### Pairing Rules

- Codes are 8 characters, uppercase, using an unambiguous alphabet (no `0`/`O`/`1`/`I`)
- Codes expire after 1 hour
- Maximum 3 pending requests per channel at a time, and at most one per sender — additional requests are declined until one expires or is approved
- Users listed in `allowedUsers` in `settings.json` skip user pairing; under `groupPolicy: "pairing"`, the group itself must still be approved
- Approved users are stored per workspace in `~/.qwen/channels/<workspace-scope>/<name>-allowlist.json` — treat this file as sensitive

## Group Chats

By default, the bot only works in direct messages. To enable group chat support, set `groupPolicy` to `"allowlist"`, `"pairing"`, or `"open"`.

### Group Policy

Controls whether the bot participates in group chats at all:

- **`disabled`** (default) — The bot ignores all group messages. Safest option.
- **`allowlist`** — The bot only responds in groups explicitly listed in `groups` by chat ID. The `"*"` key provides default settings but does **not** act as a wildcard allow.
- **`pairing`** — A deliberate mention or reply from an unknown group creates one pairing request for the group. Once approved, members can use the bot subject to the group's `senders` setting (default `open`); `privatePolicy` continues to control direct messages.
- **`open`** — The bot responds in all groups it's added to. Use with caution.

Approve a group with the same CLI command used for user pairing. The pending
request identifies the group and the member who initiated it:

```bash
qwen channel pairing approve my-channel <CODE>
```

Group approvals are stored by the group's chat ID in the channel's workspace scope. On GitHub and GitLab the chat ID is the repository/project path, so a rename or transfer detaches the stored approval — re-approve the group after renaming. A repo or project re-created under the same path inherits any stale approval — revoke group approvals after any rename, transfer, or deletion.
An unmentioned message never creates a group pairing request, even when a group
sets `requireMention` to `false`; after approval, the configured mention policy
applies normally.

Group pairing requests share the same pending queue as DM pairing requests:
a channel holds at most 3 pending requests overall, and a sender holds at most
one pending request across user and group requests (see
[Pairing Rules](#pairing-rules)).

### Mention Gating

In groups, the bot requires an `@mention` or a reply to one of its messages by default. This prevents the bot from responding to every message in a group chat.

Configure per-group with the `groups` setting:

```json
{
  "groups": {
    "*": { "requireMention": true },
    "-100123456": { "requireMention": false }
  }
}
```

- **`"*"`** — Default settings for all groups. Only sets config defaults, not an allowlist entry.
- **Group chat ID** — Override settings for a specific group. Overrides `"*"` defaults.
- **`requireMention`** (default: `true`) — When `true`, the bot only responds to messages that @mention it or reply to one of its messages. When `false`, the bot responds to all messages (useful for dedicated task groups).

### Group Senders

Admitted groups allow all members by default. Set `senders` in `groups` to restrict members independently of private access:

```json
{
  "privatePolicy": "pairing",
  "groupPolicy": "open",
  "groups": {
    "*": { "senders": "open" },
    "-100123456": { "senders": "allowlist", "allowedUsers": ["alice", "bob"] }
  }
}
```

- **`open`** (default) — Any member of an admitted group may use the bot. Private policy and user pairing have no effect on group members.
- **`allowlist`** — only the group's `allowedUsers` may speak. This list is separate from the channel-level `allowedUsers`.

All group settings use field-by-field inheritance: the specific group, then `"*"`, then the channel default where applicable, then the built-in default. Empty objects inherit defaults; arrays replace rather than merge, including `allowedUsers: []`. For example, a group that sets only `senders: "allowlist"` uses the `allowedUsers` from `"*"`. Under `groupPolicy: "allowlist"` a group ID key also admits the group, so a per-group entry there both admits the group and sets its senders.

`pairing` is deliberately not a `senders` value: pairing approvals are stored per user, so approving someone through a group message would also unlock their direct messages. Use `groupPolicy: "pairing"` to admit an entire group instead.

On the `github` and `gitlab` channels all inbound traffic is group traffic, so only `groupPolicy` and group member settings govern admission — see those channels' Security notes before using it on a public repository or project. Under `sessionScope: "single"` direct messages and groups share one conversation, so opening a group also exposes direct-message history to its members.

In the Web Shell channel editor, **Who can talk in groups** and **Allowed group member IDs** edit `groups["*"]`, and **Session operators** edits `operators`. When `senders` is unset, the editor shows its effective default and writes nothing until you pick a value. Per-group overrides and an explicitly empty `"operators": []` are set in `settings.json`; saving from the editor keeps them.

### Group History Backfill

By default, Qwen ignores unmentioned group messages and does not store them as session turns. To let the next `@mention` include recent group context, set `groupHistoryLimit` to a positive number.

```json
{
  "channels": {
    "my-dingtalk": {
      "type": "dingtalk",
      "clientId": "$DINGTALK_CLIENT_ID",
      "clientSecret": "$DINGTALK_CLIENT_SECRET",
      "groupPolicy": "open",
      "groupHistoryLimit": 50,
      "groups": {
        "*": { "requireMention": true },
        "sensitive-group-id": {
          "requireMention": true,
          "groupHistoryLimit": 0
        }
      }
    }
  }
}
```

- Omitted or `0` disables backfill.
- Group-level `groupHistoryLimit` overrides the channel-level value.
- Only messages from senders the group's `senders` setting admits are persisted (all admitted group members by default) — so `senders: "open"` also widens whose messages `groupHistoryLimit` records.
- Messages rejected by `groupPolicy` or group allowlist are not persisted.
- Pending group history is stored as local JSONL under `~/.qwen/channels/<channel-name>-group-history.jsonl` or `$QWEN_HOME/channels/<channel-name>-group-history.jsonl`.
- Cached messages are injected as untrusted context on the next real trigger and are not written as standalone session turns.

### How group messages are evaluated

```
1. groupPolicy — is this group disabled, listed, paired, or open? (no → ignore/pairing flow)
2. requireMention — was the bot mentioned/replied to? (no → no task; eligible history may be cached)
3. senders — is this member allowed?                  (open, or group member allowlist)
4. Route to session
```

### Telegram Setup for Groups

1. Add the bot to a group
2. **Disable privacy mode** in BotFather (`/mybots` → Bot Settings → Group Privacy → Turn Off) — otherwise the bot won't see non-command messages
3. **Remove and re-add the bot** to the group after changing privacy mode (Telegram caches this setting)

### Finding a Group Chat ID

To find a group's chat ID for the `groups` allowlist:

1. Stop the bot if it's running
2. Send a message mentioning the bot in the group
3. Use the Telegram Bot API to check queued updates:

```bash
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates" | python3 -m json.tool
```

Look for `message.chat.id` in the response — group IDs are negative numbers (e.g., `-5170296765`).

## Media Support

Channels support sending images and files to the agent, not just text.

### Images

Send a photo to the bot and the agent will see it — useful for sharing screenshots, error messages, or diagrams. The image is sent directly to the model as a vision input.

To use image support, configure a multimodal model for the channel:

```json
{
  "channels": {
    "my-channel": {
      "type": "telegram",
      "model": "qwen3.5-plus",
      ...
    }
  }
}
```

### Files

Send a document (PDF, code file, text file, etc.) to the bot. The file is downloaded and saved to a temporary directory, and the agent is told the file path so it can read the contents using its file-reading tools.

Files work with any model — no multimodal support required.

### Platform differences

| Feature  | Telegram                                     | WeChat                           | DingTalk                                      | Feishu                                                      | QQ Bot                                              |
| -------- | -------------------------------------------- | -------------------------------- | --------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------- |
| Images   | Direct download via Bot API                  | CDN download with AES decryption | downloadCode API (two-step)                   | Open API resources endpoint (authenticated GET, 50MB limit) | Vision input, also saved to a local path (8MB cap)  |
| Files    | Direct download via Bot API (20MB limit)     | CDN download with AES decryption | downloadCode API (two-step)                   | Open API resources endpoint (50MB limit)                    | Direct download, saved to a local path (20MB limit) |
| Captions | Photo/file captions included as message text | Not applicable                   | Rich text: mixed text + images in one message | Rich text (`post`): text extracted; embedded images ignored | Message text is kept as the caption                 |

> QQ Bot accepts images, videos, files, and voice messages. An image is passed to the agent as vision input and is also saved to a temporary local path; a video or a file is saved to a temporary local path and the agent is told that path, with a file keeping the sender's extension so the read tool can classify it. A voice message is saved as audio — the platform's SILK-to-WAV conversion is preferred when available — and needs no text to start a turn. Image input requires a model that accepts images — with a text-only model the agent still gets the path. See [QQ Bot](./qqbot#images-videos-and-files) for details.
>
> WeCom accepts text, images, mixed text plus images, files, videos, and voice messages (transcribed). Images are passed to the agent as attachments; files and videos are downloaded to temporary local paths. See [WeCom](./wecom#images-and-files) for details.

## Dispatch Modes

Controls what happens when you send a new message while the bot is still processing a previous one.

- **`steer`** (default) — The bot cancels the current request and starts working on your new message. Best for normal chat, where a follow-up usually means you want to correct or redirect the bot.
- **`collect`** — Your new messages are buffered. When the current request finishes, all buffered messages are combined into a single follow-up prompt. Good for async workflows where you want to queue up thoughts.
- **`followup`** — Each message is queued and processed as its own separate turn, in order. Useful for batch workflows where each message is independent.

```json
{
  "channels": {
    "my-channel": {
      "type": "telegram",
      "dispatchMode": "steer",
      ...
    }
  }
}
```

You can also set dispatch mode per group, overriding the channel default:

```json
{
  "groups": {
    "*": { "requireMention": true, "dispatchMode": "steer" },
    "-100123456": { "dispatchMode": "collect" }
  }
}
```

## Response delivery

Channels use their normal response delivery path. The shared delivery layer sends completed responses, and adapters may provide native progressive display, such as updating an interactive card in place. Platform message-length limits may still split long responses.

The obsolete `blockStreaming`, `blockStreamingChunk`, and `blockStreamingCoalesce` settings are no longer supported and can be removed from channel configuration. They do not affect delivery. Channel settings management rejects newly added or changed values for these fields. An unchanged stored value is retained, or removed, when the edit keeps the channel's `type`; changing a channel's `type` requires removing these fields first.

### Turn output mode

`outputMode` is a shared channel setting with adapter opt-in. Currently only **DingTalk** supports it and defaults to `per_turn` when the setting is omitted. Other adapters retain their existing behavior and receive no output-mode default: the channel editor does not offer this field, and configuration parsing or management saves reject an explicit value on unsupported adapters.

- `per_task` waits for the main task and its associated background tasks and notifications, then delivers one final result containing the last non-empty assistant reply for that task.
- `per_response` delivers each complete assistant response, not each token chunk.
- `per_turn` delivers the last non-empty assistant reply within each turn. The main turn finishes immediately when its prompt ends; later background notification turns deliver separate results.

In the default `per_turn` mode, a later background callback cannot reopen or replace the completed main result. A main result followed by eleven independent callback turns can therefore produce twelve result messages or cards. Choose `per_task` when the final result should wait for the associated background work. These modes select assistant output; they do not generate an extra summary or concatenate every intermediate reply.

The selected policy applies whether interactive cards are enabled or replies use ordinary messages. The shared layer owns output selection and task/turn coordination; native rendering, media and fallback delivery remain adapter-specific. See [DingTalk turn output mode](./dingtalk#turn-output-mode) for presentation details and the conversation scope. Channel loops and webhook runs are unchanged.

## Scheduled Channel Loops

Channels have a persistent scheduler for prompts that should run later and push
their result back to the same chat. You can ask the agent naturally, for
example, `Every 15 minutes, check the deployment and report any change`, or use
the local commands directly:

```text
/loop add "*/15 * * * *" check the deployment and report any change
/loop list
/loop inspect <id>
/loop cancel <id>
```

The agent uses the `channel_loop_create`, `channel_loop_list`, and
`channel_loop_cancel` tools when it manages these jobs for you. Schedules use
standard five-field cron expressions in the machine's local time. The job runs
unattended and its final response is delivered automatically to the chat that
created it.

Channel loops differ from the session-scoped tasks described in
[Run Prompts on a Schedule](../scheduled-tasks):

- They are stored under `$QWEN_HOME/channels/` — standalone channels use
  `cron.json` directly, while daemon-managed channels use a per-workspace file
  under `daemon/`. Both survive channel restarts.
- They are scoped to the current channel chat or thread. Each target can have up
  to 10 enabled loops, and each prompt is limited to 4,000 characters.
- They require an adapter and target that support proactive delivery. Telegram,
  DingTalk, Feishu, and WeCom opt in, subject to platform-specific target
  restrictions.
- They are unavailable with `sessionScope: "single"` because that scope is not
  tied to one chat target.
- A saved loop is disabled if its target is no longer authorized when it is due.

## Background Agent Results

When the agent delegates work to a background subagent or fork, the completion
result is delivered back to the channel chat that owns the session. Delivery
can happen after the original turn has ended, so keep the channel service or
daemon running while background work is active.

## Slash Commands

Channels support slash commands. These are handled locally (no agent round-trip):

- `/help` — List available commands
- `/clear` — Clear your session and start fresh (aliases: `/reset`, `/new`)
- `/status` — Show session info and access policy
- `/btw <question>` — Ask a side question without interrupting the current task; text-only questions up to 4096 characters, requires an agent connection with side-question support
- `/sessions [all]` — List open named tasks, or include closed tasks; available only with `multiSession: true`
- `/session current` — Show the selected named task
- `/session new <name>` — Create and select a shared-workspace task
- `/session new <name> --worktree` — Create and select a task in its own Git worktree; daemon-managed named-task mode only
- `/session use <name>` — Select an open task or reopen a closed task
- `/session cancel [<name>]` — Cancel the selected task's active prompt, or name another owned task; independently queued turns are not cancelled, but `collect`-mode follow-ups buffered behind the cancelled prompt are discarded by the existing cancellation behavior; media preparation is not targeted
- `/session close <name>` — Close a task without deleting its transcript
- `/loop add "<cron>" <prompt>` — Create a persistent scheduled channel loop
- `/loop list` — List loops for the current chat
- `/loop inspect <id>` — Show loop status and run details
- `/loop cancel <id>` — Disable a loop

All other slash commands (e.g., `/compress`, `/summary`) are forwarded to the agent. Named-task commands are registered only when the mode is enabled, so `/sessions` remains agent-visible for existing configurations.

Named-task commands work on all channel types (Telegram, WeChat, QQ, DingTalk, WeCom, Feishu, GitHub). `/cancel` is currently registered only by Telegram, and loop creation requires proactive delivery support for the current adapter and target.

## Running

```bash
# Start all configured channels (shared agent process)
qwen channel start

# Start a single channel
qwen channel start my-channel

# Check if the service is running
qwen channel status

# Stop the running service
qwen channel stop
```

The bot runs in the foreground. Press `Ctrl+C` to stop, or use `qwen channel stop` from another terminal.

### Experimental Daemon-Managed Mode

You can also run configured channels under `qwen serve`:

```bash
# Start one channel under the daemon lifecycle
qwen serve --channel my-channel

# Start all configured channels
qwen serve --channel all

# Or enable channels later on a token-protected daemon
QWEN_SERVER_TOKEN=secret qwen serve
qwen channel set my-channel --token secret

# Query or stop the daemon-managed selection
qwen channel status --daemon-url http://127.0.0.1:4170 --token secret
qwen channel stop --daemon-url http://127.0.0.1:4170 --token secret
```

This mode starts workspace-grouped channel worker processes owned by `qwen serve`. Workers connect back to the daemon through the SDK and use the same channel adapters. They are separate from the daemon process, so a channel adapter crash does not crash the daemon. An explicit `--channel` selection takes precedence and fails daemon startup if it cannot become ready. On a flagless boot, every trusted registered workspace's own `serve.channels` setting is restored, and a name that cannot be hosted is skipped with a log instead of stopping the others. `all` remains primary-workspace only. Registering a workspace later restores its channels too — without holding the registration open — once per daemon run, and not at all when the daemon was started with an explicit `--channel` or after `qwen channel stop`. Unlike the boot restore, one name it cannot host costs that workspace its whole list. Without any of these sources, the daemon does not load channel adapters or reserve the lease until the first `qwen channel set`, or until a workspace that configures its own `serve.channels` registers.

Automatic restore skips invalid startup settings and validation or lease failures that occur before workers start, while preserving unrelated settings. After a worker startup fails, the daemon continues only once cleanup succeeds. A global runtime startup timeout or an unconfirmed worker stop still follows the normal startup-failure path; the service lease remains held while worker termination is unconfirmed. Check the daemon log for messages identifying `serve.channels` when a channel does not restore.

Stored startup names must be non-empty, have no leading or trailing whitespace, and contain no unsafe control or invisible characters. Invalid entries are skipped individually and logged by their array index. Startup does not rename instances or rewrite the configuration. The channel startup toggle reflects the saved setting; runtime state shows whether the channel is running.

`qwen serve --channel` is not the same service as `qwen channel start`. Standalone `qwen channel start` still uses the ACP-backed channel service and can run channel configs with different `cwd` values. Daemon-managed channels require every selected channel's `cwd` to resolve to a workspace registered by the daemon. In multi-workspace mode, a selection replacement keeps workers for workspaces whose ordered channel list did not change; `all` remains primary-workspace-only.

Without `--daemon-url`, `qwen channel status` and `qwen channel stop` retain standalone pidfile behavior. Their `--daemon-url` variants query or stop the daemon manager. Runtime selections are not written to settings and do not survive daemon restart. If a ready worker exits unexpectedly, the daemon continues running and reports a channel-worker warning in `/daemon/status`.

## Webhook-triggered tasks

Daemon-managed channels can also accept authenticated webhook events. Qwen receives the event as context, summarizes and decides what matters, and then delivers the final response to the configured chat target. This is not a raw notification relay.
Webhook tasks require `approvalMode: "yolo"` because they run without interactive approval. That setting applies to the whole channel, not only webhook turns, so use a dedicated webhook channel or tightly restrict normal chat senders for that channel.

Example channel config:

```json
{
  "channels": {
    "dingtalk-main": {
      "type": "dingtalk",
      "clientId": "$DINGTALK_CLIENT_ID",
      "clientSecret": "$DINGTALK_CLIENT_SECRET",
      "cwd": "/repo",
      "privatePolicy": "allowlist",
      "allowedUsers": ["12345"],
      "approvalMode": "yolo",
      "sessionScope": "user",
      "webhooks": {
        "sources": {
          "github-ci": {
            "secretEnv": "QWEN_CHANNEL_GITHUB_CI_SECRET",
            "targets": {
              "operator": {
                "chatId": "DINGTALK_USER_ID",
                "senderId": "webhook:github-ci",
                "isGroup": false
              },
              "team": {
                "chatId": "OPEN_CONVERSATION_ID",
                "senderId": "webhook:github-ci",
                "isGroup": true
              }
            }
          }
        }
      }
    }
  }
}
```

For DingTalk, set `isGroup` explicitly on every target. A direct-message target uses the DingTalk user ID as `chatId` with `isGroup: false`; a group target uses the group `openConversationId` with `isGroup: true`. Other adapters may require their own proactive target shape.

Daemon-managed DingTalk, Feishu, Telegram, and WeCom channels dynamically observe contacts from authorized inbound messages. List contacts observed in the primary workspace during the default seven-day freshness window:

```bash
curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" \
  http://127.0.0.1:4170/workspace/channel/observed-contacts
```

Use `GET /workspaces/:workspace/channel/observed-contacts` to select another registered, trusted workspace. Add `?freshWithinSeconds=N` to choose a window from one second through 365 days. The daemon advertises this API with the `workspace_channel_observed_contacts` capability.

The response returns complete platform IDs and labels. Group labels use names already present in accepted inbound messages when available: DingTalk supplies `conversationTitle`, and Telegram supplies `chat.title`. Feishu and WeCom group labels currently fall back to their complete IDs; no platform directory or group-detail API is queried. Topic labels also fall back to complete IDs. Each `lastObservedAt` is a canonical ISO 8601 UTC timestamp with millisecond precision; clients can convert it to the user's local time zone for display. Top-level `users` contains users observed in direct messages. `groups` contains observed group conversations, `groups[].users` contains users observed in each group, and `groups[].topics[].users` contains users observed in Feishu or Telegram topics:

```json
{
  "users": [
    {
      "channelName": "feishu-main",
      "label": "Example User",
      "id": "ou_complete_user_id",
      "lastObservedAt": "2026-07-17T08:00:00.000Z"
    }
  ],
  "groups": [
    {
      "channelName": "feishu-main",
      "label": "oc_complete_chat_id",
      "id": "oc_complete_chat_id",
      "lastObservedAt": "2026-07-17T08:05:00.000Z",
      "users": [
        {
          "label": "Example User",
          "id": "ou_complete_user_id",
          "lastObservedAt": "2026-07-17T08:05:00.000Z"
        }
      ],
      "topics": []
    }
  ]
}
```

These nested users are observed participants, not authoritative group membership. Only messages that pass direct/group, mention, sender, and pairing gates are recorded. Repeated observations refresh labels and timestamps; passive observation cannot detect a leave or deletion until the relationship becomes stale. Message content is never stored. The bounded registry lives under `$QWEN_HOME/channels/daemon/<workspaceHash>/observed-contacts.json`, outside the workspace checkout and partitioned per workspace. Its 500-observation limit is shared by all channels and conversations in that workspace, and observations older than 365 days are removed on the next accepted write. If the registry becomes malformed or uses an unsupported version, delete that file to reset it; accepted traffic recreates it. Webhook configuration and delivery are unchanged.

Start `qwen serve` with the channel worker enabled:

```bash
QWEN_SERVER_TOKEN="$QWEN_SERVER_TOKEN" qwen serve --require-auth --channel dingtalk-main
```

Example request:

```bash
curl -X POST "http://127.0.0.1:4170/channels/dingtalk-main/webhooks/github-ci" \
  -H "x-qwen-webhook-secret: $QWEN_CHANNEL_GITHUB_CI_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "push",
    "targetRef": "operator",
    "title": "CI pipeline finished",
    "payload": {
      "targetRef": "refs/heads/main",
      "repository": "qwen-code",
      "status": "success"
    }
  }'
```

Webhook routes authenticate with the webhook secret header, even when `qwen serve` is running with bearer auth enabled. Do not share the daemon bearer token with webhook providers. Webhook config and `secretEnv` values are loaded when the daemon starts; restart `qwen serve` after changing webhook sources or rotating secrets. A `202 {"accepted": true}` response means the channel worker accepted ownership of the task, not that the final response has already been delivered to chat. Check daemon and channel worker logs, plus `/daemon/status`, when troubleshooting delivery failures.

### Multi-Channel Mode

When you run `qwen channel start` without a name, all channels defined in `settings.json` start together sharing a single agent process. Each channel maintains its own sessions — a Telegram user and a WeChat user get separate conversations, even though they share the same agent.

Each channel uses its own `cwd` from its config, so different channels can work on different projects simultaneously.

### Service Management

The channel service uses a PID file (`~/.qwen/channels/service.pid`) to track the running instance:

- **Duplicate prevention**: Running `qwen channel start` while a service is already running will show an error instead of starting a second instance
- **`qwen channel stop`**: Gracefully stops the running service from another terminal
- **`qwen channel status`**: Shows whether the service is running, its uptime, and session counts per channel

### Crash Recovery

If the agent process crashes unexpectedly, the channel service automatically restarts it and attempts to restore all active sessions. Users can continue their conversations without starting over.

- Sessions are persisted to `~/.qwen/channels/sessions.json` while the service is running
- On crash: the agent restarts within 3 seconds and reloads saved sessions
- After 3 consecutive crashes, the service exits with an error
- On clean shutdown (Ctrl+C or `qwen channel stop`): session data is cleared — the next start is always fresh

---

### Design Openrouter Auth And Models (`docs/design/openrouter-auth-and-models.md`)

# OpenRouter Auth and Model Management Design

This document captures the design intent behind the OpenRouter auth flow and the
model management changes introduced with it. It intentionally focuses on the
product and architectural choices, not implementation history.

## Goals

- Let users authenticate with OpenRouter from both CLI and `/auth`.
- Reuse the existing OpenAI-compatible provider path instead of adding a new auth
  type for OpenRouter.
- Make the first-run experience usable without asking users to manage hundreds of
  models immediately.
- Keep a clear path toward richer model management via `/manage-models`.

## OpenRouter Auth

OpenRouter is integrated as an OpenAI-compatible provider:

- auth type: `AuthType.USE_OPENAI`
- provider settings: `modelProviders.openai`
- API key env var: `OPENROUTER_API_KEY`
- base URL: `https://openrouter.ai/api/v1`

This avoids introducing an OpenRouter-specific `AuthType` when the runtime model
provider path is already OpenAI-compatible. It keeps auth status, model
resolution, provider selection, and settings schema aligned with the existing
provider abstraction.

The user-facing flows are:

- `/auth` → OpenRouter for the interactive TUI flow.
- Environment variables for automation or direct API-key setup:
  `OPENROUTER_API_KEY` plus `OPENAI_BASE_URL=https://openrouter.ai/api/v1`.
- `~/.qwen/settings.json` for scripted setup that needs explicit model provider
  entries.

Browser OAuth uses OpenRouter's PKCE flow and writes the exchanged API key into
settings before refreshing auth as `AuthType.USE_OPENAI`.

## Model Management

OpenRouter exposes a large dynamic model catalog. Writing every discovered model
into `modelProviders.openai` would make `/model` noisy and would turn a long-term
settings field into a cache of a remote catalog.

The key design split is:

- **Catalog**: the full set of models discovered from a source such as
  OpenRouter.
- **Enabled set**: the smaller set of models that should appear in `/model` and
  be persisted in user settings.

For the initial OpenRouter flow, auth should finish with a useful default enabled
set instead of interrupting the user with a large picker. The recommended set
should be small, stable, and biased toward models that let users try the product
successfully, including free models when available.

`/model` remains a fast model switcher. It should not become the place where
users browse and curate a full provider catalog.

## `/manage-models`

Richer model management belongs in a separate `/manage-models` entry point. That
flow should let users:

- browse discovered models;
- search by id, display name, provider prefix, and derived tags such as `free` or
  `vision`;
- see which models are currently enabled;
- enable or disable models in batches.

The source dimension must remain part of this design. OpenRouter is only the
first dynamic catalog source; future sources such as ModelScope and ModelStudio
should fit the same shape. UI complexity can be reduced, but the underlying
source abstraction should stay available as the extension point.

## Current Boundary

This change should do the minimum needed to make OpenRouter auth and model setup
pleasant:

- OAuth or key-based auth configures OpenRouter through the existing
  OpenAI-compatible provider path.
- The initial enabled model set is curated instead of dumping the full catalog
  into settings.
- Full catalog storage, browsing, filtering, and batch management are deferred to
  `/manage-models`.

The design principle is simple: authentication should get users to a working
state quickly, while model curation should live in a dedicated management flow.

---

### Developers Architecture (`docs/developers/architecture.md`)

# Qwen Code Architecture Overview

Qwen Code is a monorepo that supports an interactive terminal, headless and
programmatic execution, the Agent Client Protocol (ACP), a long-running HTTP
daemon, web and IDE clients, and messaging-channel adapters. This document maps
those surfaces to the packages that implement them and explains the main
runtime boundaries.

For detailed daemon internals, start with the
[daemon documentation](./daemon/00-index.md). For HTTP request and event
shapes, see the [`qwen serve` protocol reference](./qwen-serve-protocol.md).

## System at a glance

Qwen Code has two agent execution models:

- **Direct execution:** the interactive TUI and headless CLI construct and run
  the agent runtime directly.
- **ACP execution:** `qwen --acp` hosts the agent behind an ACP transport. It
  can be driven by an ACP client directly or by `qwen serve` through the shared
  ACP bridge.

`qwen serve` adds an HTTP + Server-Sent Events (SSE) control plane around ACP
execution so multiple clients can use long-lived, workspace-scoped runtimes.

```mermaid
flowchart TB
    subgraph surfaces["User and integration surfaces"]
        TUI["Interactive TUI / headless CLI"]
        PQ["TypeScript SDK process client"]
        WEB["Web Shell / shared Web UI"]
        IDE["IDE integrations"]
        CHANNEL["Messaging channels"]
        CUSTOM["Custom daemon clients"]
    end

    subgraph hosts["Process and transport hosts"]
        CLI["CLI host<br/>packages/cli"]
        SDK["Daemon client<br/>packages/sdk-typescript"]
        SERVE["qwen serve<br/>packages/cli/src/serve"]
        BRIDGE["ACP bridge<br/>packages/acp-bridge"]
        ACP["qwen --acp child"]
    end

    subgraph runtime["Agent runtime"]
        CORE["Agent orchestration and tools<br/>packages/core"]
    end

    subgraph external["External systems"]
        MODEL["Model providers"]
        MCP["MCP servers"]
        HOST["Workspace filesystem and processes"]
    end

    TUI --> CLI
    PQ --> CLI
    CLI --> CORE

    WEB --> SDK
    IDE --> SDK
    CHANNEL --> SDK
    CUSTOM --> SDK
    SDK --> SERVE
    SERVE --> BRIDGE
    BRIDGE --> ACP
    ACP --> CORE

    CORE --> MODEL
    CORE --> MCP
    CORE --> HOST
```

The diagram shows the main production paths. Some adapters also have standalone
modes: for example, `qwen channel start` uses the ACP bridge without requiring
an HTTP daemon. See the
[channel plugin guide](./channel-plugins.md#runtime-modes) for those variants.

## Repository layout

| Path                                                                                                       | Responsibility                                                                                                                                                                                   |
| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `packages/cli`                                                                                             | The `qwen` executable, argument parsing, configuration assembly, Ink TUI, headless output, ACP entry point, `qwen serve`, and command-specific adapters.                                         |
| `packages/core`                                                                                            | UI-independent agent orchestration, model-provider integration, prompt and context construction, tool registration and execution, permissions, sessions, memory, telemetry, and shared services. |
| `packages/acp-bridge`                                                                                      | ACP channel lifecycle, session multiplexing, event delivery, permission mediation, process spawning, and the filesystem seam shared by daemon and adapter hosts.                                 |
| `packages/sdk-typescript`                                                                                  | Programmatic process execution through `query()` plus HTTP/SSE clients and transcript projection for `qwen serve`.                                                                               |
| `packages/web-shell`                                                                                       | The browser UI and daemon React adapter built on the TypeScript SDK.                                                                                                                             |
| `packages/web-templates`                                                                                   | Web templates packaged as embeddable JavaScript and CSS strings.                                                                                                                                 |
| `packages/audio-capture`                                                                                   | Native microphone capture for voice input.                                                                                                                                                       |
| `packages/channels`                                                                                        | The shared channel runtime and platform adapters for messaging services.                                                                                                                         |
| `packages/desktop`, `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension` | Product and editor surfaces that adapt Qwen Code to their host environments.                                                                                                                     |
| `packages/sdk-java`, `packages/sdk-python`                                                                 | Language-specific programmatic clients.                                                                                                                                                          |
| `packages/cua-driver`, `packages/mobile-mcp`                                                               | Computer-use and mobile-device integrations exposed through MCP-compatible boundaries.                                                                                                           |
| `integration-tests`                                                                                        | End-to-end coverage for CLI, interactive, SDK, sandbox, hook, and terminal behavior.                                                                                                             |
| `docs` and `docs-site`                                                                                     | User, developer, protocol, and design documentation plus the documentation site.                                                                                                                 |
| `scripts`                                                                                                  | Build, packaging, release, validation, and repository-maintenance automation.                                                                                                                    |

Most code lives in npm workspaces under `packages/`. A package should depend on
another package through its declared public exports rather than through a
relative path into that package's source tree.

## Package boundaries

### CLI and presentation surfaces

`packages/cli` owns the executable and chooses the runtime mode from command-line
arguments. It loads user and workspace settings, constructs the core
configuration, enters the requested sandbox when necessary, and then starts one
of the interactive, headless, ACP, daemon, channel, or maintenance flows.

Presentation remains outside the core runtime:

- the Ink TUI renders local interactive sessions;
- `packages/web-shell` adapts daemon state to React providers and hooks and
  provides the browser experience;
- IDE and channel packages translate host-specific events into shared client or
  bridge contracts.

### Core runtime

`packages/core` owns the agent loop. It constructs model requests, maintains
conversation context, dispatches tool calls, applies permission policy, and
returns structured events and results to the active host. Built-in tools cover
file operations, shell execution, search, planning, web access, memory, skills,
and subagents. MCP extends the tool and resource surface without coupling the
runtime to a specific integration.

The core package does not decide how results are displayed or how a remote
client transports them. Those decisions belong to the CLI, bridge, SDK, and UI
layers.

### ACP bridge

`packages/acp-bridge` connects a host process to an ACP agent runtime. Its main
responsibilities are:

- spawning or attaching to an ACP channel;
- multiplexing sessions and clients;
- forwarding prompts, cancellations, and ACP notifications;
- mediating permission requests;
- publishing bounded session event streams;
- providing a workspace filesystem interface to the host.

The bridge can use a real `qwen --acp` child process in production or an
in-memory channel in tests. See the
[`@qwen-code/acp-bridge` README](../../packages/acp-bridge/README.md) for its
public entry points.

### SDK and UI adapters

The TypeScript SDK exposes two client styles:

- `query()` starts and controls a Qwen Code process for programmatic local use;
- daemon clients communicate with `qwen serve` over HTTP and SSE.

`packages/web-shell` builds a React state layer and browser UI on the daemon
client. Other clients,
including IDE integrations and daemon-managed channels, reuse the same SDK and
event contracts instead of importing server implementation code.

## Runtime flows

### Direct CLI flow

1. The CLI parses arguments and resolves user, workspace, environment, and
   command-line configuration.
2. It prepares sandboxing and constructs the core runtime configuration.
3. The core runtime builds the model request and enters the agent/tool loop.
4. Tool calls are checked against permission policy and executed in the active
   workspace environment.
5. The CLI renders incremental events in the TUI or serializes them for
   headless output.

### Daemon flow

1. A client uses the TypeScript SDK or the documented HTTP API to connect to
   `qwen serve`.
2. The daemon authenticates the request and resolves the workspace that owns
   the requested operation.
3. The workspace runtime forwards agent operations through its ACP bridge to a
   `qwen --acp` child.
4. The child runs the same core agent and tool logic used by direct execution.
5. Responses and notifications return through the bridge; session events are
   delivered to clients over SSE.

With multi-workspace sessions enabled, each live workspace runtime owns its own
bridge and ACP child. Filesystem access, environment overlays, MCP transports,
sessions, and failure handling remain scoped to that resolved runtime. The
[daemon architecture](./daemon/01-architecture.md) documents the process
topology, trust boundaries, event replay, and lifecycle in detail.

## Extension points

Qwen Code can be extended at several layers:

- **MCP servers** add tools, prompts, and resources to the core runtime.
- **Extensions and skills** package reusable commands, configuration, and agent
  behavior.
- **Channel plugins** adapt messaging platforms to the shared channel runtime.
- **SDK clients** build custom local or daemon-backed applications.
- **UI adapters** project shared daemon events into host-specific state and
  presentation.

Keep platform-specific concerns in adapters. Shared agent behavior belongs in
the core runtime, while transport and wire behavior belongs in the ACP bridge,
SDK, or daemon host.

## Configuration and state

The CLI assembles effective configuration from command-line arguments,
environment variables, user settings, workspace settings, and defaults before
constructing the runtime. The core receives the resolved configuration rather
than reading presentation-specific input. See
[Settings](../users/configuration/settings.md) for the supported settings and
their scopes.

Direct sessions persist their history and metadata through shared core
services. In daemon mode, the daemon resolves the owning workspace and exposes
workspace- and session-scoped operations to clients; the ACP child remains the
owner of live agent execution.

## Where to go next

- [Daemon developer documentation](./daemon/00-index.md)
- [`qwen serve` HTTP protocol](./qwen-serve-protocol.md)
- [TypeScript SDK](../../packages/sdk-typescript/README.md)
- [ACP bridge](../../packages/acp-bridge/README.md)
- [Channel plugin developer guide](./channel-plugins.md)
- [Tool development](./tools/introduction.md)
- [Integration testing](./development/integration-tests.md)

---

### Developers Channel Plugins (`docs/developers/channel-plugins.md`)

# Channel Plugin Developer Guide

A channel plugin connects Qwen Code to a messaging platform. It's packaged as an [extension](../users/extension/introduction) and loaded at startup. For user-facing docs on installing and configuring plugins, see [Plugins](../users/features/channels/plugins).

## How It Fits Together

Your plugin sits in the Platform Adapter layer. You handle platform-specific concerns (connecting, receiving messages, sending responses). `ChannelBase` handles everything else (access control, session routing, prompt queuing, slash commands, crash recovery).

```
Your Plugin  →  builds Envelope  →  handleInbound()
ChannelBase  →  gates → commands → routing → ChannelAgentBridge.prompt()
ChannelBase  →  calls your sendMessage() with the agent's response
```

`ChannelAgentBridge` is the adapter-facing bridge contract. The current standalone `qwen channel start` path provides an `AcpBridge`, but plugin code should type constructor parameters as `ChannelAgentBridge` so the same adapter can run behind other bridge implementations later.

Migration note for existing TypeScript plugins: if your adapter constructor or factory explicitly types `bridge` as `AcpBridge`, change that annotation to `ChannelAgentBridge` and keep using only the methods exposed by that contract. JavaScript plugins are unaffected at runtime, and standalone `qwen channel start` still passes the current `AcpBridge` implementation.

## Runtime Modes

The same plugin adapter can be hosted by either channel runtime:

- `qwen channel start [name]` is the standalone ACP-backed service. It still uses `AcpBridge` and remains the stable command for running channels outside a daemon.
- `qwen serve --channel <name>` and repeatable `--channel` flags start experimental daemon-managed channel workers. Named channels are grouped by owning workspace, with one worker per owning runtime. `--channel all` intentionally starts only the primary workspace's configured channels. Workers are owned by `qwen serve`, connect to that daemon through the SDK, and pass adapters a `ChannelAgentBridge` facade backed by `DaemonChannelBridge`.

Daemon-managed channels inherit the daemon's lifecycle and status reporting. They are intentionally out-of-process so adapter or platform SDK failures do not crash the daemon. Every named channel must resolve to exactly one registered, trusted workspace; its worker receives that runtime's canonical cwd and environment overlay. A user/system channel with no cwd is ambiguous when several workspaces are registered, while a channel in a workspace-local settings file belongs to that workspace by default. `--channel all` remains primary-only and cannot be combined with named selections.

## The Plugin Object

Your extension entry point exports a `plugin` conforming to `ChannelPlugin`:

```typescript
import type { ChannelPlugin } from '@qwen-code/channel-base';
import { MyChannel } from './MyChannel.js';

export const plugin: ChannelPlugin = {
  channelType: 'my-platform', // Unique ID, used in settings.json "type" field
  displayName: 'My Platform', // Shown in CLI output
  requiredConfigFields: ['apiKey'], // Validated at startup (beyond standard ChannelConfig)
  createChannel: (name, config, bridge, options) =>
    new MyChannel(name, config, bridge, options),
};
```

## The Channel Adapter

Extend `ChannelBase` and implement three methods:

```typescript
import { ChannelBase } from '@qwen-code/channel-base';
import type {
  ChannelBaseOptions,
  ChannelAgentBridge,
  ChannelConfig,
  Envelope,
  SessionTarget,
} from '@qwen-code/channel-base';

export class MyChannel extends ChannelBase {
  constructor(
    name: string,
    config: ChannelConfig,
    bridge: ChannelAgentBridge,
    options?: ChannelBaseOptions,
  ) {
    super(name, config, bridge, options);
  }

  async connect(): Promise<void> {
    // Connect to your platform, register message handlers
    // When a message arrives:
    const envelope: Envelope = {
      channelName: this.name,
      senderId: '...', // Stable, unique platform user ID
      senderName: '...', // Display name
      chatId: '...', // Chat/conversation ID (distinct for DMs vs groups)
      text: '...', // Message text (strip @mentions)
      isGroup: false, // Accurate — used by GroupGate
      isMentioned: false, // Accurate — used by GroupGate
      isReplyToBot: false, // Accurate — used by GroupGate
    };
    this.handleInbound(envelope);
  }

  async sendMessage(chatId: string, text: string): Promise<void> {
    // Format markdown → platform format, chunk if needed, deliver
  }

  disconnect(): void {
    // Clean up connections
  }
}
```

Most adapters should pass `options` through unchanged. If an adapter creates its own `SessionRouter` and passes that router to `super()`, set `registerBridgeEvents: true` in `ChannelBaseOptions` so `ChannelBase` still receives `toolCall` and `sessionDied` events directly. Leave it unset for routers supplied by the channel gateway.

If your adapter exposes shell-command or BTW side-question behavior, check that the corresponding `bridge.shellCommand` / `bridge.btw` method exists before enabling it. Daemon-managed workers omit those optional methods unless the daemon advertises the matching `session_shell_command` / `session_btw` capability.

## The Envelope

The normalized message object you build from platform data. The boolean flags drive gate logic, so they must be accurate.

| Field            | Type         | Required | Notes                                                                      |
| ---------------- | ------------ | -------- | -------------------------------------------------------------------------- |
| `channelName`    | string       | Yes      | Use `this.name`                                                            |
| `senderId`       | string       | Yes      | Must be stable across messages (used for session routing + access control) |
| `senderName`     | string       | Yes      | Display name                                                               |
| `chatId`         | string       | Yes      | Must distinguish DMs from groups                                           |
| `chatName`       | string       | No       | Group/conversation name when supplied by the platform                      |
| `text`           | string       | Yes      | Strip bot @mentions                                                        |
| `threadId`       | string       | No       | For `sessionScope: "thread"`                                               |
| `messageId`      | string       | No       | Platform message ID — useful for response correlation                      |
| `isGroup`        | boolean      | Yes      | GroupGate relies on this                                                   |
| `isMentioned`    | boolean      | Yes      | GroupGate relies on this                                                   |
| `isReplyToBot`   | boolean      | Yes      | GroupGate relies on this                                                   |
| `referencedText` | string       | No       | Quoted message — prepended as context                                      |
| `imageBase64`    | string       | No       | Base64-encoded image (legacy — prefer `attachments`)                       |
| `imageMimeType`  | string       | No       | e.g., `image/jpeg` (legacy — prefer `attachments`)                         |
| `attachments`    | Attachment[] | No       | Structured media attachments (see below)                                   |

### Attachments

Use the `attachments` array for images, files, audio, and video. `handleInbound()` resolves them automatically: images with base64 `data` are sent to the model as vision input, files with a `filePath` get their path appended to the prompt so the agent can read them.

```typescript
interface Attachment {
  type: 'image' | 'file' | 'audio' | 'video';
  data?: string; // base64-encoded data (images, small files)
  filePath?: string; // absolute path to local file (large files saved to disk)
  mimeType: string; // e.g. 'application/pdf', 'image/jpeg'
  fileName?: string; // original file name from the platform
}
```

Example — handling a file upload in your adapter:

```typescript
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';

const buf = await downloadFromPlatform(fileId);
const dir = join(tmpdir(), 'channel-files');
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
const filePath = join(dir, fileName);
writeFileSync(filePath, buf);

envelope.attachments = [
  {
    type: 'file',
    filePath,
    mimeType: 'application/pdf',
    fileName,
  },
];
```

The legacy `imageBase64`/`imageMimeType` fields still work for backwards compatibility but `attachments` is preferred for new code.

## Extension Manifest

Your `qwen-extension.json` declares the channel type. The key must match `channelType` in your plugin object:

```json
{
  "name": "my-channel-extension",
  "version": "1.0.0",
  "channels": {
    "my-platform": {
      "entry": "dist/index.js",
      "displayName": "My Platform Channel"
    }
  }
}
```

## Optional Extension Points

**Custom slash commands** — register in your constructor:

```typescript
this.registerCommand('mycommand', async (envelope, args) => {
  await this.sendMessage(envelope.chatId, 'Response');
  return true; // handled, don't forward to agent
});
```

**Working indicators** — override `onPromptStart()` and `onPromptEnd()` to show platform-specific typing indicators. These hooks fire only when a prompt actually begins processing — not for buffered messages (collect mode) or gated/blocked messages:

```typescript
protected override onPromptStart(chatId: string, sessionId: string, messageId?: string): void {
  this.platformClient.sendTyping(chatId); // your platform API
}

protected override onPromptEnd(chatId: string, sessionId: string, messageId?: string): void {
  this.platformClient.stopTyping(chatId);
}
```

**Tool call hooks** — override `onToolCall()` to display agent activity (e.g., "Running shell command...").

**Streaming hooks** — override `onResponseChunk(chatId, chunk, sessionId, segment)` for per-chunk progressive display (e.g., editing a message in-place). Override `onResponseComplete(chatId, fullText, sessionId, segment)` to customize final delivery. In daemon-managed named-task mode, `segment.sourceLabel` is immutable delivery metadata for that segment. Render it once on each independently visible message or card, including a separately visible final response, but do not add it to raw buffers or model text. Clear adapter-owned segment state from `onOutputSegmentEnd()`.

**Named-task attribution** — `sendThreadMessage(chatId, threadId, text, sourceLabel)` receives the same optional plain-text label for one-shot and proactive delivery boundaries. The default implementation handles plain messages. Adapters that override delivery, split messages, emit cards, or provide fallback sends must repeat the label at every independently visible boundary, escape only the label for the target markup dialect, and include its rendered size in platform limits. Run no-reply checks, media-marker projection, audit hashing, transcript persistence, and retry-body capture against the raw response before presentation; if delivery is persisted for restart-safe retry, persist the captured label separately.

Interactive `ChannelUserInputRequestContext` also carries `sourceLabel`. Cards, terminal replacements, and plain fallbacks must retain it without weakening the existing request, session, run, owner, and target checks.

**Proactive delivery** — override `supportsProactiveSend()` to return `true` when the adapter can send without an active inbound request. `ChannelBase` uses this capability for persistent channel loops, webhook tasks, background-agent results, and daemon delivery. The default target policy rejects threaded targets; override the protected target checks only for target shapes your platform can deliver safely:

```typescript
override supportsProactiveSend(): boolean {
  return true;
}

protected override supportsProactiveTarget(target: SessionTarget): boolean {
  return target.threadId === undefined;
}

protected override async pushProactive(
  target: SessionTarget,
  text: string,
  sourceLabel?: string,
): Promise<void> {
  await this.sendThreadMessage(
    target.chatId,
    target.threadId,
    text,
    sourceLabel,
  );
}
```

Use `supportsProactiveDeliveryTarget()` when generic daemon delivery accepts a different target shape, and `supportsProactiveWebhookTarget()` when webhook delivery differs from loops and background results. Keep unsupported targets rejected rather than falling back to another conversation.

**Media** — populate `envelope.attachments` with images/files. See [Attachments](#attachments) above.

## Reference Implementations

- **Plugin example** (`packages/channels/plugin-example/`) — minimal WebSocket-based adapter, good starting point
- **Telegram** (`packages/channels/telegram/`) — full-featured: images, files, formatting, typing indicators
- **DingTalk** (`packages/channels/dingtalk/`) — stream-based with rich text handling

---

### Developers Contributing (`docs/developers/contributing.md`)

# How to Contribute

We would love to accept your patches and contributions to this project.

## Contribution Process

### Code Reviews

All submissions, including submissions by project members, require review. We
use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests)
for this purpose.

### Pull Request Guidelines

To help us review and merge your PRs quickly, please follow these guidelines. PRs that do not meet these standards may be closed.

#### 1. Link to an Existing Issue

All PRs should be linked to an existing issue in our tracker. This ensures that every change has been discussed and is aligned with the project's goals before any code is written.

- **For bug fixes:** The PR should be linked to the bug report issue.
- **For features:** The PR should be linked to the feature request or proposal issue that has been approved by a maintainer.

If an issue for your change doesn't exist, please **open one first** and wait for feedback before you start coding.

#### 2. Keep It Small and Focused

We favor small, atomic PRs that address a single issue or add a single, self-contained feature.

- **Do:** Create a PR that fixes one specific bug or adds one specific feature.
- **Don't:** Bundle multiple unrelated changes (e.g., a bug fix, a new feature, and a refactor) into a single PR.

As a rule of thumb, start splitting a PR once it exceeds about 1,200 changed
lines. PRs above about 2,000 changed lines should either be split into a series
of smaller, logical PRs that can be reviewed and merged independently, or
explain in the PR description why the change needs to land together.

#### 3. Use Draft PRs for Work in Progress

If you'd like to get early feedback on your work, please use GitHub's **Draft Pull Request** feature. This signals to the maintainers that the PR is not yet ready for a formal review but is open for discussion and initial feedback.

#### 4. Ensure All Checks Pass

Before submitting your PR, ensure that all automated checks are passing by running `npm run preflight`. This command runs all tests, linting, and other style checks.

#### 5. Update Documentation

If your PR introduces a user-facing change (e.g., a new command, a modified flag, or a change in behavior), you must also update the relevant documentation in the `/docs` directory.

New or updated design documents must include complete English (`<name>.md`) and
Simplified Chinese (`<name>.zh-CN.md`) versions in the same directory and PR.
Keep their structure, decisions, constraints, and acceptance criteria aligned,
and add reciprocal language links. See the design documentation requirements
in [English](../design/README.md) or [简体中文](../design/README.zh-CN.md).

#### 6. Write Clear Commit Messages and a Good PR Description

Your PR should have a clear, descriptive title and a detailed description of the changes. Follow the [Conventional Commits](https://www.conventionalcommits.org/) standard for your commit messages.

- **Good PR Title:** `feat(cli): Add --json flag to 'config get' command`
- **Bad PR Title:** `Made some changes`

In the PR description, explain the "why" behind your changes and link to the relevant issue (e.g., `Fixes #123`).

## Development Setup and Workflow

This section guides contributors on how to build, modify, and understand the development setup of this project.

### Setting Up the Development Environment

**Prerequisites:**

1.  **Node.js**:
    - **Development:** Please use Node.js `>=22`. Ink 7 (used by the TUI) requires Node 22, and `react@^19.2.0` is the matching peer. You can use a tool like [nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions.
    - **Production:** For running the CLI in a production environment, any version of Node.js `>=22` is acceptable.
2.  **Git**

### Build Process

To clone the repository:

```bash
git clone https://github.com/QwenLM/qwen-code.git # Or your fork's URL
cd qwen-code
```

To install dependencies defined in `package.json` as well as root dependencies:

```bash
npm install
```

To build the entire project (all packages):

```bash
npm run build
```

This command typically compiles TypeScript to JavaScript, bundles assets, and prepares the packages for execution. Refer to `scripts/build.js` and `package.json` scripts for more details on what happens during the build.

### Enabling Sandboxing

[Sandboxing](#sandboxing) is highly recommended and requires, at a minimum, setting `QWEN_SANDBOX=true` in your `~/.env` and ensuring a sandboxing provider (e.g. `macOS Seatbelt`, `docker`, or `podman`) is available. See [Sandboxing](#sandboxing) for details.

To build both the `qwen` CLI utility and the sandbox container, run `build:all` from the root directory:

```bash
npm run build:all
```

To skip building the sandbox container, you can use `npm run build` instead.

### Running

To start the Qwen Code application from the source code (after building), run the following command from the root directory:

```bash
npm start
```

If you'd like to run the source build outside of the qwen-code folder, you can utilize `npm link path/to/qwen-code/packages/cli` (see: [docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) to run with `qwen`

### Running Tests

This project contains two types of tests: unit tests and integration tests.

#### Unit Tests

To execute the unit test suite for the project:

```bash
npm run test
```

This will run tests located in the `packages/core` and `packages/cli` directories. Ensure tests pass before submitting any changes. For a more comprehensive check, it is recommended to run `npm run preflight`.

#### Integration Tests

The integration tests are designed to validate the end-to-end functionality of Qwen Code. They are not run as part of the default `npm run test` command.

To run the integration tests, use the following command:

```bash
npm run test:e2e
```

For more detailed information on the integration testing framework, please see the [Integration Tests documentation](./development/integration-tests.md).

### Linting and Preflight Checks

To ensure code quality and formatting consistency, run the preflight check:

```bash
npm run preflight
```

This command will run ESLint, Prettier, all tests, and other checks as defined in the project's `package.json`.

_ProTip_

after cloning create a git precommit hook file to ensure your commits are always clean.

```bash
echo "
# Run npm build and check for errors
if ! npm run preflight; then
  echo "npm build failed. Commit aborted."
  exit 1
fi
" > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
```

#### Formatting

To separately format the code in this project by running the following command from the root directory:

```bash
npm run format
```

This command uses Prettier to format the code according to the project's style guidelines.

#### Linting

To separately lint the code in this project, run the following command from the root directory:

```bash
npm run lint
```

### Coding Conventions

- Please adhere to the coding style, patterns, and conventions used throughout the existing codebase.
- **Imports:** Pay special attention to import paths. The project uses ESLint to enforce restrictions on relative imports between packages.

### Project Structure

- `packages/`: Contains the individual sub-packages of the project.
  - `cli/`: The command-line interface.
  - `core/`: The core backend logic for Qwen Code.
- `docs/`: Contains all project documentation.
- `scripts/`: Utility scripts for building, testing, and development tasks.

For more detailed architecture, see `docs/architecture.md`.

## Documentation Development

This section describes how to develop and preview the documentation locally.

### Prerequisites

1. Ensure you have Node.js (version 22+) installed
2. Have npm or yarn available

### Setup Documentation Site Locally

To work on the documentation and preview changes locally:

1. Navigate to the `docs-site` directory:

   ```bash
   cd docs-site
   ```

2. Install dependencies:

   ```bash
   npm install
   ```

3. Link the documentation content from the main `docs` directory:

   ```bash
   npm run link
   ```

   This creates a symbolic link from `../docs` to `content` in the docs-site project, allowing the documentation content to be served by the Next.js site.

4. Start the development server:

   ```bash
   npm run dev
   ```

5. Open [http://localhost:3000](http://localhost:3000) in your browser to see the documentation site with live updates as you make changes.

Any changes made to the documentation files in the main `docs` directory will be reflected immediately in the documentation site.

## Debugging

### VS Code:

0.  Run the CLI to interactively debug in VS Code with `F5`
1.  Start the CLI in debug mode from the root directory:
    ```bash
    npm run debug
    ```
    This command runs `node --inspect-brk dist/index.js` within the `packages/cli` directory, pausing execution until a debugger attaches. You can then open `chrome://inspect` in your Chrome browser to connect to the debugger.
2.  In VS Code, use the "Attach" launch configuration (found in `.vscode/launch.json`).

Alternatively, you can use the "Launch Program" configuration in VS Code if you prefer to launch the currently open file directly, but 'F5' is generally recommended.

To hit a breakpoint inside the sandbox container run:

```bash
DEBUG=1 qwen
```

**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect `qwen` due to automatic exclusion. Use `.qwen/.env` files for `qwen`-specific debug settings.

### React DevTools

To debug the CLI's React-based UI, you can use React DevTools. Ink, the library used for the CLI's interface, is compatible with React DevTools version 4.x.

1.  **Start the Qwen Code application in development mode:**

    ```bash
    DEV=true npm start
    ```

2.  **Install and run React DevTools version 4.28.5 (or the latest compatible 4.x version):**

    You can either install it globally:

    ```bash
    npm install react-devtools@4.28.5
    react-devtools
    ```

    Or run it directly using npx:

    ```bash
    npx react-devtools@4.28.5
    ```

    Your running CLI application should then connect to React DevTools.

## Sandboxing

> TBD

## Manual Publish

We publish an artifact for each commit to our internal registry. But if you need to manually cut a local build, then run the following commands:

```
npm run clean
npm install
npm run auth
npm run prerelease:dev
npm publish --workspaces
```

---

### Developers Roadmap (`docs/developers/roadmap.md`)

# Qwen Code RoadMap

> **Objective**: Catch up with Claude Code's product functionality, continuously refine details, and enhance user experience.

| Category                        | Phase 1                                                                                                                                                                            | Phase 2                                                                                                                                                                                     |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| User Experience                 | ✅ Terminal UI<br>✅ Support OpenAI Protocol<br>✅ Settings<br>✅ OAuth<br>✅ Cache Control<br>✅ Memory<br>✅ Compress<br>✅ Theme                                                | Better UI<br>OnBoarding<br>LogView<br>✅ Session<br>Permission<br>🔄 Cross-platform Compatibility<br>✅ Coding Plan<br>✅ Anthropic Provider<br>✅ Multimodal Input<br>✅ Unified Web Shell |
| Coding Workflow                 | ✅ Slash Commands<br>✅ MCP<br>✅ PlanMode<br>✅ TodoWrite<br>✅ SubAgent<br>✅ Multi Model<br>✅ Chat Management<br>✅ Tools (WebFetch, Bash, TextSearch, FileReadFile, EditFile) | 🔄 Hooks<br>✅ Skill<br>✅ Headless Mode<br>✅ Tools (WebSearch)<br>✅ LSP Support<br>✅ Concurrent Runner                                                                                  |
| Building Open Capabilities      | ✅ Custom Commands                                                                                                                                                                 | ✅ QwenCode SDK<br>✅ Extension System                                                                                                                                                      |
| Integrating Community Ecosystem |                                                                                                                                                                                    | ✅ VSCode Plugin<br>✅ ACP/Zed<br>✅ GHA                                                                                                                                                    |
| Administrative Capabilities     | ✅ Stats<br>✅ Feedback                                                                                                                                                            | Costs<br>Dashboard<br>✅ User Feedback Dialog                                                                                                                                               |

> For more details, please see the list below.

## Features

#### Completed Features

| Feature                 | Version   | Description                                             | Category                        | Phase |
| ----------------------- | --------- | ------------------------------------------------------- | ------------------------------- | ----- |
| **Coding Plan**         | `V0.10.0` | Alibaba Cloud Coding Plan authentication & models       | User Experience                 | 2     |
| Unified Web Shell       | `V0.9.0`  | Shared browser UI for VS Code and `qwen serve`          | User Experience                 | 2     |
| Export Chat             | `V0.8.0`  | Export sessions to Markdown/HTML/JSON/JSONL             | User Experience                 | 2     |
| Extension System        | `V0.8.0`  | Full extension management with slash commands           | Building Open Capabilities      | 2     |
| LSP Support             | `V0.7.0`  | Experimental LSP service (`--experimental-lsp`)         | Coding Workflow                 | 2     |
| Anthropic Provider      | `V0.7.0`  | Anthropic API provider support                          | User Experience                 | 2     |
| User Feedback Dialog    | `V0.7.0`  | In-app feedback collection with fatigue mechanism       | Administrative Capabilities     | 2     |
| Concurrent Runner       | `V0.6.0`  | Batch CLI execution with Git integration                | Coding Workflow                 | 2     |
| Multimodal Input        | `V0.6.0`  | Image, PDF, audio, video input support                  | User Experience                 | 2     |
| Skill                   | `V0.6.0`  | Extensible custom AI skills (experimental)              | Coding Workflow                 | 2     |
| GitHub Actions          | `V0.5.0`  | qwen-code-action and automation                         | Integrating Community Ecosystem | 1     |
| VSCode Plugin           | `V0.5.0`  | VSCode extension plugin                                 | Integrating Community Ecosystem | 1     |
| QwenCode SDK            | `V0.4.0`  | Open SDK for third-party integration                    | Building Open Capabilities      | 1     |
| Session                 | `V0.4.0`  | Enhanced session management                             | User Experience                 | 1     |
| i18n                    | `V0.3.0`  | Internationalization and multilingual support           | User Experience                 | 1     |
| Headless Mode           | `V0.3.0`  | Headless mode (non-interactive)                         | Coding Workflow                 | 1     |
| ACP/Zed                 | `V0.2.0`  | ACP and Zed editor integration                          | Integrating Community Ecosystem | 1     |
| Terminal UI             | `V0.1.0+` | Interactive terminal user interface                     | User Experience                 | 1     |
| Settings                | `V0.1.0+` | Configuration management system                         | User Experience                 | 1     |
| Theme                   | `V0.1.0+` | Multi-theme support                                     | User Experience                 | 1     |
| Support OpenAI Protocol | `V0.1.0+` | Support for OpenAI API protocol                         | User Experience                 | 1     |
| Chat Management         | `V0.1.0+` | Session management (save, restore, browse)              | Coding Workflow                 | 1     |
| MCP                     | `V0.1.0+` | Model Context Protocol integration                      | Coding Workflow                 | 1     |
| Multi Model             | `V0.1.0+` | Multi-model support and switching                       | Coding Workflow                 | 1     |
| Slash Commands          | `V0.1.0+` | Slash command system                                    | Coding Workflow                 | 1     |
| Tool: Bash              | `V0.1.0+` | Shell command execution tool (with is_background param) | Coding Workflow                 | 1     |
| Tool: FileRead/EditFile | `V0.1.0+` | File read/write and edit tools                          | Coding Workflow                 | 1     |
| Custom Commands         | `V0.1.0+` | Custom command loading                                  | Building Open Capabilities      | 1     |
| Feedback                | `V0.1.0+` | Feedback mechanism (/bug command)                       | Administrative Capabilities     | 1     |
| Stats                   | `V0.1.0+` | Usage statistics and quota display                      | Administrative Capabilities     | 1     |
| Memory                  | `V0.0.9+` | Project-level and global memory management              | User Experience                 | 1     |
| Cache Control           | `V0.0.9+` | Prompt caching control (Anthropic, DashScope)           | User Experience                 | 1     |
| PlanMode                | `V0.0.14` | Task planning mode                                      | Coding Workflow                 | 1     |
| Compress                | `V0.0.11` | Chat compression mechanism                              | User Experience                 | 1     |
| SubAgent                | `V0.0.11` | Dedicated sub-agent system                              | Coding Workflow                 | 1     |
| TodoWrite               | `V0.0.10` | Task management and progress tracking                   | Coding Workflow                 | 1     |
| Tool: TextSearch        | `V0.0.8+` | Text search tool (grep, supports .qwenignore)           | Coding Workflow                 | 1     |
| Tool: WebFetch          | `V0.0.7+` | Web content fetching tool                               | Coding Workflow                 | 1     |
| Tool: WebSearch         | `V0.0.7+` | Built-in web search (DashScope Responses API)           | Coding Workflow                 | 1     |
| OAuth                   | `V0.0.5+` | OAuth login authentication (Qwen OAuth)                 | User Experience                 | 1     |

#### Features to Develop

| Feature                      | Priority | Status      | Description                       | Category                    |
| ---------------------------- | -------- | ----------- | --------------------------------- | --------------------------- |
| Better UI                    | P1       | Planned     | Optimized terminal UI interaction | User Experience             |
| OnBoarding                   | P1       | Planned     | New user onboarding flow          | User Experience             |
| Permission                   | P1       | Planned     | Permission system optimization    | User Experience             |
| Cross-platform Compatibility | P1       | In Progress | Windows/Linux/macOS compatibility | User Experience             |
| LogView                      | P2       | Planned     | Log viewing and debugging feature | User Experience             |
| Hooks                        | P2       | In Progress | Extension hooks system            | Coding Workflow             |
| Costs                        | P2       | Planned     | Cost tracking and analysis        | Administrative Capabilities |
| Dashboard                    | P2       | Planned     | Management dashboard              | Administrative Capabilities |

#### Distinctive Features to Discuss

| Feature          | Status   | Description                                           |
| ---------------- | -------- | ----------------------------------------------------- |
| Home Spotlight   | Research | Project discovery and quick launch                    |
| Competitive Mode | Research | Competitive mode                                      |
| Pulse            | Research | User activity pulse analysis (OpenAI Pulse reference) |
| Code Wiki        | Research | Project codebase wiki/documentation system            |

---

### Developers Sdk Java (`docs/developers/sdk-java.md`)

# Qwen Code Java SDK

The Qwen Code Java SDK provides a recommended daemon transport for `qwen serve` and retains the experimental legacy stdio API for compatibility. Both APIs ship in the same `com.alibaba:qwencode-sdk` artifact.

## Requirements

- Java >= 11 for `0.1.0-alpha`
- Maven >= 3.9.2 when building or publishing this SDK from source
- A compatible `qwen serve` for the daemon API, or qwen-code >= 0.5.0 for the legacy stdio API

### Dependencies

- **Logging API**: org.slf4j:slf4j-api (choose an SLF4J provider in your application)
- **Utilities**: org.apache.commons:commons-lang3
- **JSON Processing**: Fastjson2 for encoding and Jackson Core for strict decoding
- **Testing**: JUnit 5 (org.junit.jupiter:junit-jupiter)

## Installation

Add the following dependency to your Maven `pom.xml`:

```xml
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>qwencode-sdk</artifactId>
    <version>0.1.0-alpha</version>
</dependency>
```

Or if using Gradle, add to your `build.gradle`:

```gradle
implementation 'com.alibaba:qwencode-sdk:0.1.0-alpha'
```

## Building and Running

### Build Commands

```bash
# Compile the project
mvn compile

# Run tests
mvn test

# Package the JAR
mvn package

# Install to local repository
mvn install
```

### Real daemon E2E from source

Run the real-daemon Java integration tests from the repository root after building both the workspaces and the root CLI bundle:

```bash
npm run build
npm run bundle
npx tsx scripts/run-java-daemon-sdk-e2e.ts
```

`npm run build` alone does not refresh `dist/cli.js`; the E2E harness launches that bundle and fails with an explicit prerequisite error when it is missing.

## Recommended daemon API

Start `qwen serve`, then create an independent thread-scoped session. `promptText` returns only after a matching `turn_complete`; incomplete streams fail with `PromptOutcomeIndeterminateException` rather than returning partial text as success.

For the lifecycle guarantees assumed by `0.1.0-alpha`, use the qwen-code build released from the same source revision as the SDK. The daemon must contain the idempotent per-client detach ledger from [#7386](https://github.com/QwenLM/qwen-code/pull/7386), the per-epoch terminal guarantee from [#7400](https://github.com/QwenLM/qwen-code/pull/7400), and this release's acknowledged admission cancellation plus FIFO cancel-drain fence. The #7400 commit alone is not sufficient: a same-wire daemon can acknowledge cancel before agent dispatch without stopping the admitted prompt, or let an unacknowledged session-scoped cancel reach a queued successor. The bundled ACP child uses one acknowledged admission-aware cancellation handshake; a custom standards-compliant ACP child without that extension receives one standard `session/cancel` notification. Feature negotiation cannot distinguish older same-wire daemon builds, so the SDK fails closed rather than reporting partial output as success.

The bundled cancellation handshake deliberately waits for the targeted prompt call to settle before the daemon dispatches its queued successor. It has no timeout that merely acknowledges cancellation: doing so could let a late session-scoped cancel reach the next prompt. If a provider, tool, or custom integration ignores its `AbortSignal` indefinitely, the cancel mutation can therefore remain outcome-unknown and that session must not be reused. Treat a formal prompt terminal received within the caller's observation boundary as authoritative; otherwise close or destroy the session after observation fails. Recovering a wedged shared ACP child without disturbing its sibling sessions requires stronger runtime isolation and is outside this alpha contract.

```java
import com.alibaba.qwen.code.daemon.DaemonClient;
import com.alibaba.qwen.code.daemon.DaemonSessionClient;
import com.alibaba.qwen.code.daemon.PromptTextResult;
import java.net.URI;

try (DaemonClient daemon = DaemonClient.builder()
        .baseUri(URI.create("http://127.0.0.1:4170"))
        .build();
     DaemonSessionClient session = daemon.createSession()) {
    PromptTextResult result = session.promptText("Explain this repository");
    System.out.println(result.getText());
}
```

Callers that need to allocate the session identity before creation can pass an RFC UUID v1-v5. The SDK checks `session_id_override` before the mutation and reports a different returned ID as `SessionCreationOutcomeUnknownException`:

```java
CreateSessionRequest request = CreateSessionRequest.builder()
        .sessionId("550E8400-E29B-41D4-A716-446655440000")
        .build();

try (DaemonSessionClient session = daemon.createSession(request)) {
    System.out.println(session.getSession().getSessionId());
}
```

The daemon normalizes the ID to lowercase and creates a new thread session. This is not an idempotent attach; after an ambiguous create outcome, recover with the known ID rather than retrying creation.

If `qwen serve` requires authentication, add
`.bearerToken(System.getenv("QWEN_SERVER_TOKEN"))` to the `DaemonClient`
builder. The SDK sends the bearer on REST and SSE requests and never puts it in
the URL.

Use `startPrompt` with a `PromptObserver` when you need ordered text, thought, tool, usage, permission, and raw event callbacks. Its `acceptanceFuture()` and `completionFuture()` views separately expose daemon admission and the reliable turn terminal. `respondToPermission()` returns `false` when the request was already resolved or no longer pending. Cancelling the future views does not cancel the daemon prompt; use `cancelActivePrompt()` for the session-level daemon cancel operation and still wait for the matching terminal. A cooperative cancellation completes with `turn_complete` and `stopReason=cancelled`; `promptText()` returns its `PromptTextResult`, so callers that distinguish cancellation must inspect `result.getTerminal().getStopReason()`. If the agent or provider fails while cancelling, the daemon can instead publish `turn_error`, which makes `promptText()` throw `PromptTurnException`.

When cancellation, deadline, teardown, or agent settlement race, the daemon's exactly-once latch publishes the first formal terminal and suppresses later candidates. Always branch on the received terminal itself; the last control mutation sent by the client does not determine the terminal kind or error code.

The SSE transport sends `Accept-Encoding: identity` and `Last-Event-ID`, validates framing and event IDs, deduplicates replay, and reconnects only the SSE GET. Prompt and other mutation requests are never retried automatically. HTTP 408 and 5xx responses to prompt admission, session creation, permission, cancel, heartbeat, detach, or delete are reported as outcome-unknown because they do not prove that the daemon rejected the mutation. Finite response bodies and SSE observation have independent deadlines.

Creation-time model selection is intentionally not exposed by the Java daemon SDK API in this alpha. The daemon reports a rejected `modelServiceId` only as an SSE event emitted before the create response, while this SDK opens its stream from the later prompt-admission watermark. Until the daemon returns a definitive create result or the SDK owns a separate session-event subscription from `Last-Event-ID: 0`, use the daemon's configured default model.

`PromptRequest.Builder.deadline(Duration)` requests a daemon-enforced prompt deadline and is accepted only when the daemon advertises `prompt_absolute_deadline`; otherwise the SDK fails before sending the prompt. The value must be between 1 and 2,147,483,647 milliseconds, matching the daemon's Node timer range. This is separate from `observationTimeout(Duration)`, which only bounds local SSE observation and never sends a cancel mutation.

Before creating a session, the SDK requires the daemon to advertise the REST transport and `session_scope_override`; this prevents an older daemon from silently ignoring the requested `thread` scope and attaching the client to a shared session. When a caller supplies a session ID, the SDK additionally requires `session_id_override` before sending the mutation. When `client_heartbeat` is advertised, an open session sends a fresh heartbeat every minute so the daemon does not reap an otherwise idle client. Set `heartbeatInterval(Duration.ZERO)` on the `DaemonClient` builder to disable this behavior, or choose a different positive interval. A heartbeat is never retried; the next scheduled heartbeat is a separate keepalive. Prompt observation is bounded to 32 concurrent prompts per client by default and can be adjusted with `maximumConcurrentPrompts`. Admission and terminal future callbacks run away from transport workers; callbacks that remain blocked consume bounded publication capacity. SSE stream cleanup is also bounded, and a close that remains blocked retains its cleanup reservation. Either condition can cause a later `startPrompt` to fail with `DaemonClientCapacityException` rather than dropping a timeout close or growing threads and queued work without limit.

An indeterminate completion is an outcome boundary, not a session-reuse boundary. After `PromptAdmissionUnknownException` or `PromptOutcomeIndeterminateException`, that `DaemonSessionClient` permanently rejects further prompts even if local stream cleanup later succeeds; close or destroy the session instead. An observation timeout is published without waiting forever for a blocked stream close, while cleanup continues asynchronously and retains bounded client capacity until it finishes.

## Legacy stdio API

The existing `com.alibaba.qwen.code.cli` API remains available:

```java
public static void runSimpleExample() {
    List<String> result = QwenCodeCli.simpleQuery("hello world");
    result.forEach(logger::info);
}
```

For more advanced usage with custom transport options:

```java
public static void runTransportOptionsExample() {
    TransportOptions options = new TransportOptions()
            .setModel("qwen3-coder-flash")
            .setPermissionMode(PermissionMode.AUTO_EDIT)
            .setCwd("./")
            .setEnv(new HashMap<String, String>() {{put("CUSTOM_VAR", "value");}})
            .setIncludePartialMessages(true)
            .setTurnTimeout(new Timeout(120L, TimeUnit.SECONDS))
            .setMessageTimeout(new Timeout(90L, TimeUnit.SECONDS))
            .setAllowedTools(Arrays.asList("read_file", "write_file", "glob"));

    List<String> result = QwenCodeCli.simpleQuery("who are you, what are your capabilities?", options);
    result.forEach(logger::info);
}
```

For streaming content handling with custom content consumers:

```
/* Detailed source-code truncated for AI context efficiency. */
```

other examples see src/test/java/com/alibaba/qwen/code/cli/example

## Java 11 migration and alpha limits

`0.1.0-alpha` raises the minimum Java version for the whole artifact from 8 to 11. Java 8 applications must remain on `0.0.3-alpha`. Logback is no longer a runtime dependency; add the SLF4J provider your application uses.

This alpha deliberately fails closed when it cannot prove a prompt terminal. It does not guarantee exactly-once execution across daemon restarts, automatic epoch recovery, snapshot/resync, persisted cursors, or true prompt-ID-targeted cancellation. `prompt_cancelled` and queue events are advisory; only matching `turn_complete` and `turn_error` are terminal.

If session creation has an ambiguous transport outcome, the daemon may retain a session whose ID never reached the caller. The SDK does not retry creation and cannot detach that unknown session; daemon-side lifecycle reaping is the recovery boundary.

## Architecture

The artifact contains two isolated implementations:

- **Daemon API**: `DaemonClient` and `DaemonSessionClient` use REST mutations plus resumable SSE and own bounded HTTP, prompt, maintenance, and timer resources.
- **Legacy stdio API**: `QwenCodeCli`, `Session`, and `ProcessTransport` manage a child CLI process using the existing CLI protocol DTOs and utilities.

The daemon implementation does not reuse the legacy process transport, session model, DTOs, or global executor.

## Legacy stdio features

### Permission Modes

The SDK supports different permission modes for controlling tool execution:

- **`default`**: Write tools are denied unless approved via `canUseTool` callback or in `allowedTools`. Read-only tools execute without confirmation.
- **`plan`**: Blocks all write tools, instructing AI to present a plan first.
- **`auto-edit`**: Auto-approve edit tools (`edit`, `write_file`, `notebook_edit`) while other tools require confirmation.
- **`yolo`**: All tools execute automatically without confirmation.

### Session Event Consumers and Assistant Content Consumers

The SDK provides two key interfaces for handling events and content from the CLI:

#### SessionEventConsumers Interface

The `SessionEventConsumers` interface provides callbacks for different types of messages during a session:

- `onSystemMessage`: Handles system messages from the CLI (receives Session and SDKSystemMessage)
- `onResultMessage`: Handles result messages from the CLI (receives Session and SDKResultMessage)
- `onAssistantMessage`: Handles assistant messages (AI responses) (receives Session and SDKAssistantMessage)
- `onPartialAssistantMessage`: Handles partial assistant messages during streaming (receives Session and SDKPartialAssistantMessage)
- `onUserMessage`: Handles user messages (receives Session and SDKUserMessage)
- `onOtherMessage`: Handles other types of messages (receives Session and String message)
- `onControlResponse`: Handles control responses (receives Session and CLIControlResponse)
- `onControlRequest`: Handles control requests (receives Session and CLIControlRequest, returns CLIControlResponse)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlRequest<CLIControlPermissionRequest>, returns Behavior)

#### AssistantContentConsumers Interface

The `AssistantContentConsumers` interface handles different types of content within assistant messages:

- `onText`: Handles text content (receives Session and TextAssistantContent)
- `onThinking`: Handles thinking content (receives Session and ThinkingAssistantContent)
- `onToolUse`: Handles tool use content (receives Session and ToolUseAssistantContent)
- `onToolResult`: Handles tool result content (receives Session and ToolResultAssistantContent)
- `onOtherContent`: Handles other content types (receives Session and AssistantContent)
- `onUsage`: Handles usage information (receives Session and AssistantUsage)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlPermissionRequest, returns Behavior)
- `onOtherControlRequest`: Handles other control requests (receives Session and ControlRequestPayload, returns ControlResponsePayload)

#### Relationship Between the Interfaces

**Important Note on Event Hierarchy:**

- `SessionEventConsumers` is the **high-level** event processor that handles different message types (system, assistant, user, etc.)
- `AssistantContentConsumers` is the **low-level** content processor that handles different types of content within assistant messages (text, tools, thinking, etc.)

**Processor Relationship:**

- `SessionEventConsumers` → `AssistantContentConsumers` (SessionEventConsumers uses AssistantContentConsumers to process content within assistant messages)

**Event Derivation Relationships:**

- `onAssistantMessage` → `onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`, `onUsage`
- `onPartialAssistantMessage` → `onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`
- `onControlRequest` → `onPermissionRequest`, `onOtherControlRequest`

**Event Timeout Relationships:**

Each event handler method has a corresponding timeout method that allows customizing the timeout behavior for that specific event:

- `onSystemMessage` ↔ `onSystemMessageTimeout`
- `onResultMessage` ↔ `onResultMessageTimeout`
- `onAssistantMessage` ↔ `onAssistantMessageTimeout`
- `onPartialAssistantMessage` ↔ `onPartialAssistantMessageTimeout`
- `onUserMessage` ↔ `onUserMessageTimeout`
- `onOtherMessage` ↔ `onOtherMessageTimeout`
- `onControlResponse` ↔ `onControlResponseTimeout`
- `onControlRequest` ↔ `onControlRequestTimeout`

For AssistantContentConsumers timeout methods:

- `onText` ↔ `onTextTimeout`
- `onThinking` ↔ `onThinkingTimeout`
- `onToolUse` ↔ `onToolUseTimeout`
- `onToolResult` ↔ `onToolResultTimeout`
- `onOtherContent` ↔ `onOtherContentTimeout`
- `onPermissionRequest` ↔ `onPermissionRequestTimeout`
- `onOtherControlRequest` ↔ `onOtherControlRequestTimeout`

**Default Timeout Values:**

- `SessionEventSimpleConsumers` default timeout: 180 seconds (Timeout.TIMEOUT_180_SECONDS)
- `AssistantContentSimpleConsumers` default timeout: 60 seconds (Timeout.TIMEOUT_60_SECONDS)

**Timeout Hierarchy Requirements:**

For proper operation, the following timeout relationships should be maintained:

- `onAssistantMessageTimeout` return value should be greater than `onTextTimeout`, `onThinkingTimeout`, `onToolUseTimeout`, `onToolResultTimeout`, and `onOtherContentTimeout` return values
- `onControlRequestTimeout` return value should be greater than `onPermissionRequestTimeout` and `onOtherControlRequestTimeout` return values

### Transport Options

The `TransportOptions` class allows configuration of how the SDK communicates with the Qwen Code CLI:

- `pathToQwenExecutable`: Path to the Qwen Code CLI executable
- `cwd`: Working directory for the CLI process
- `model`: AI model to use for the session
- `permissionMode`: Permission mode that controls tool execution
- `env`: Environment variables to pass to the CLI process
- `maxSessionTurns`: Limits the number of conversation turns in a session
- `coreTools`: A non-empty allowlist of core tools. An omitted or empty list is treated as unspecified; an empty list does not disable tools. MCP and other non-core tools have separate registration and permission rules. Use `excludeTools` to exclude named tools explicitly; `allowedTools` controls approval rather than registration.
- `excludeTools`: List of tools to exclude from being available to the AI
- `allowedTools`: List of tools that are pre-approved for use without additional confirmation
- `authType`: Authentication type to use for the session
- `includePartialMessages`: Enables receiving partial messages during streaming responses
- `turnTimeout`: Timeout for a complete turn of conversation
- `messageTimeout`: Timeout for individual messages within a turn
- `resumeSessionId`: ID of a previous session to resume
- `otherOptions`: Additional command-line options to pass to the CLI

### Session Control Features

- **Session creation**: Use `QwenCodeCli.newSession()` to create a new session with custom options
- **Session management**: The `Session` class provides methods to send prompts, handle responses, and manage session state
- **Session cleanup**: Always close sessions using `session.close()` to properly terminate the CLI process
- **Session resumption**: Use `setResumeSessionId()` in `TransportOptions` to resume a previous session
- **Session interruption**: Use `session.interrupt()` to interrupt a currently running prompt
- **Dynamic model switching**: Use `session.setModel()` to change the model during a session
- **Dynamic permission mode switching**: Use `session.setPermissionMode()` to change the permission mode during a session

### Thread Pool Configuration

The SDK uses a thread pool for managing concurrent operations with the following default configuration:

- **Core Pool Size**: 30 threads
- **Maximum Pool Size**: 100 threads
- **Keep-Alive Time**: 60 seconds
- **Queue Capacity**: 300 tasks (using LinkedBlockingQueue)
- **Thread Naming**: "qwen_code_cli-pool-{number}"
- **Daemon Threads**: true
- **Rejected Execution Handler**: CallerRunsPolicy

Tasks still running or queued in the default pool are abandoned when the JVM exits. Callers that require completion must wait for those tasks explicitly.

## Error Handling

The SDK provides specific exception types for different error scenarios:

- `SessionControlException`: Thrown when there's an issue with session control, including attempting to use a closed or unavailable session. Session construction and `start()` can throw it directly; `QwenCodeCli.newSession()` wraps lower-level creation and initialization failures in a `RuntimeException`.
- `SessionSendPromptException`: Thrown when there's an issue sending a prompt or receiving a response

## FAQ / Troubleshooting

### Q: Do I need to install the Qwen CLI separately?

A: Yes. The daemon API requires a compatible `qwen serve`; the legacy stdio
API requires qwen-code 0.5.0 or higher.

### Q: What Java versions are supported?

A: `0.1.0-alpha` requires Java 11 or higher. Java 8 users must remain on `0.0.3-alpha`.

### Q: How do I handle long-running requests?

A: The SDK includes timeout utilities. You can configure timeouts using the `Timeout` class in `TransportOptions`.

### Q: Why are some tools not executing?

A: This is likely due to permission modes. Check your permission mode settings and consider using `allowedTools` to pre-approve certain tools.

### Q: How do I resume a previous session?

A: Use the `setResumeSessionId()` method in `TransportOptions` to resume a previous session.

### Q: Can I customize the environment for the CLI process?

A: Yes, use the `setEnv()` method in `TransportOptions` to pass environment variables to the CLI process.

## License

Apache-2.0 - see [LICENSE](../../LICENSE) for details.

---

### Developers Sdk Python (`docs/developers/sdk-python.md`)

# Python SDK

## `qwen-code-sdk`

`qwen-code-sdk` is an experimental Python SDK for Qwen Code. v1 targets the
existing `stream-json` CLI protocol and keeps the transport surface small and
testable.

## Scope

- Package name: `qwen-code-sdk`
- Import path: `qwen_code_sdk`
- Runtime requirement: Python `>=3.10`
- CLI dependency: external `qwen` executable is required in v1
- Transport scope: process transport only
- Not included in v1: ACP transport, SDK-embedded MCP servers

## Install

```bash
pip install qwen-code-sdk
```

For preview releases:

```bash
pip install --pre qwen-code-sdk
```

If `qwen` is not on `PATH`, pass `path_to_qwen_executable` explicitly.
On Windows, the SDK discovers npm's `qwen.cmd` shim only through the effective
child `PATH`, ignoring extensionless shell shims and non-launchable `PATHEXT`
associations. A command in the current directory is considered only when that
directory is explicitly included in `PATH`.
You can override the lookup path through the `env` option; Windows environment
variable names are case-insensitive, so `PATH`, `Path`, and `path` are equivalent.
Closing a running Windows shim stops its CLI process tree through the system
`taskkill` utility; process exit and stderr cleanup waits have bounded timeouts.

Before writing SDK code, make sure the CLI works in the same shell:

```bash
qwen --version
```

## Quick Start

```python
import asyncio

from qwen_code_sdk import (
    is_sdk_assistant_message,
    is_sdk_result_message,
    query,
)


def extract_text(message):
    content = message.get("message", {}).get("content", [])
    if not isinstance(content, list):
        return repr(content)
    texts = [
        block.get("text", "")
        for block in content
        if isinstance(block, dict) and block.get("type") == "text"
    ]
    return "".join(texts) if texts else "[no text content]"


def print_result(message):
    if message.get("is_error"):
        error = message.get("error") or {}
        print(f"Error: {error.get('message', 'Unknown error')}")
        return
    print(message.get("result", ""))


async def main() -> None:
    async with query(
        "Explain the repository structure.",
        {
            "cwd": "/path/to/project",
            "path_to_qwen_executable": "qwen",
        },
    ) as result:
        async for message in result:
            if is_sdk_assistant_message(message):
                print(extract_text(message))
            elif is_sdk_result_message(message):
                print_result(message)


asyncio.run(main())
```

`asyncio.run()` is appropriate for standalone scripts. If your application
already runs an event loop, such as Jupyter, FastAPI, or pytest-asyncio, call
`await main()` instead.

## Sync Usage

Use `query_sync` when your host application is not async:

```python
from qwen_code_sdk import is_sdk_result_message, query_sync


with query_sync(
    "Summarize this repository in one paragraph.",
    {
        "cwd": "/path/to/project",
        "path_to_qwen_executable": "qwen",
    },
) as result:
    for message in result:
        if is_sdk_result_message(message):
            if message.get("is_error"):
                error = message.get("error") or {}
                print(f"Error: {error.get('message', 'Unknown error')}")
            else:
                print(message.get("result", ""))
```

## API Surface

### Top-level entry points

- `query(prompt, options=None) -> Query`
- `query_sync(prompt, options=None) -> SyncQuery`

`prompt` supports either:

- `str` for single-turn requests
- `AsyncIterable[SDKUserMessage]` for multi-turn streams

### `Query`

- Async iterable over SDK messages
- `close()`
- `interrupt()`
- `set_model(model)`
- `set_permission_mode(mode)`
- `supported_commands()`
- `mcp_server_status()`
- `get_session_id()`
- `is_closed()`

### `QueryOptions`

| Option                     | Type / values                                              | Description                                                                                                                                                       |
| -------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cwd`                      | `str`                                                      | Working directory for the CLI process.                                                                                                                            |
| `model`                    | `str`                                                      | Model override for this SDK session.                                                                                                                              |
| `path_to_qwen_executable`  | `str`                                                      | `qwen`, an explicit binary path, or a `.js` CLI bundle.                                                                                                           |
| `permission_mode`          | `default`, `plan`, `auto-edit`, `auto`, `yolo`             | Tool execution approval mode. `auto` lets an LLM classifier approve tool calls; `yolo` auto-approves all tools; use it only in trusted or sandboxed environments. |
| `can_use_tool`             | async callback                                             | Custom permission callback for tool requests.                                                                                                                     |
| `env`                      | `dict[str, str]`                                           | Extra environment variables passed to the CLI process.                                                                                                            |
| `system_prompt`            | `str`                                                      | Override the system prompt.                                                                                                                                       |
| `append_system_prompt`     | `str`                                                      | Append extra instructions to the system prompt.                                                                                                                   |
| `debug`                    | `bool`                                                     | Forward CLI stderr to stderr when no `stderr` hook exists.                                                                                                        |
| `max_session_turns`        | `int`                                                      | Maximum turns before the CLI ends the session.                                                                                                                    |
| `core_tools`               | `list[str]`                                                | Non-empty core-tool allowlist. Omitted or empty lists are unspecified; `core_tools=[]` does not disable tools.                                                    |
| `exclude_tools`            | `list[str]`                                                | Exclude matching tools.                                                                                                                                           |
| `allowed_tools`            | `list[str]`                                                | Allow matching tools without callback approval.                                                                                                                   |
| `auth_type`                | `openai`, `anthropic`, `qwen-oauth`, `gemini`, `vertex-ai` | Authentication mode passed to the CLI.                                                                                                                            |
| `include_partial_messages` | `bool`                                                     | Emit partial assistant stream events.                                                                                                                             |
| `resume`                   | UUID string                                                | Resume a known session id.                                                                                                                                        |
| `continue_session`         | `bool`                                                     | Continue the latest CLI session.                                                                                                                                  |
| `session_id`               | UUID string                                                | Start or correlate a session with a known id.                                                                                                                     |
| `timeout`                  | mapping                                                    | Timeouts in seconds.                                                                                                                                              |
| `stderr`                   | callable                                                   | Receives CLI stderr lines.                                                                                                                                        |

Use only one of `resume`, `continue_session`, or `session_id` in a request. The
SDK raises `ValidationError` if these session options are combined.

Unsupported in v1:

- `mcp_servers`

### Common Configuration

```python
options = {
    "cwd": "/path/to/project",
    "path_to_qwen_executable": "qwen",
    "model": "qwen-plus",
    "permission_mode": "plan",
    "max_session_turns": 1,
    "env": {
        "OPENAI_MODEL": "qwen-plus",
    },
    "timeout": {
        "control_request": 60,
        "can_use_tool": 60,
        "stream_close": 60,
    },
}
```

Timeout values are seconds. `env` is merged on top of the parent process
environment, so you only need to pass variables that should differ for this SDK
session. Set secrets such as `OPENAI_API_KEY` in the parent environment or a
secrets manager rather than hardcoding them in source.

## Permission Handling

When the CLI emits a `can_use_tool` control request, the SDK routes it through
`can_use_tool(tool_name, tool_input, context)`.

- Default behavior: deny
- Default timeout: 60 seconds, configurable with `timeout.can_use_tool`
- Timeout fallback: deny
- Callback exceptions: converted to deny with an error message
- Callback context: `cancel_event`, `suggestions`, and `blocked_path`
- Callback contract: `can_use_tool` must be async with 3 positional arguments;
  `stderr` must accept 1 positional string argument

Example:

```
/* Detailed source-code truncated for AI context efficiency. */
```

If you do not pass `can_use_tool`, the SDK denies permission requests by
default.

## Multi-Turn Sessions

For multi-turn sessions, pass an async iterable of `SDKUserMessage` objects:

```python
import asyncio

from qwen_code_sdk import SDKUserMessage, is_sdk_result_message, query

SESSION_ID = "123e4567-e89b-12d3-a456-426614174000"


async def prompts():
    first: SDKUserMessage = {
        "type": "user",
        "session_id": SESSION_ID,
        "message": {
            "role": "user",
            "content": "Create a concise project summary.",
        },
        "parent_tool_use_id": None,
    }
    yield first

    second: SDKUserMessage = {
        "type": "user",
        "session_id": SESSION_ID,
        "message": {
            "role": "user",
            "content": "Also list the test files.",
        },
        "parent_tool_use_id": None,
    }
    yield second


async def main():
    async with query(
        prompts(),
        {
            "cwd": "/path/to/project",
            "path_to_qwen_executable": "qwen",
            "session_id": SESSION_ID,
        },
    ) as result:
        async for message in result:
            if is_sdk_result_message(message):
                if message.get("is_error"):
                    error = message.get("error") or {}
                    print(f"Error: {error.get('message', 'Unknown error')}")
                else:
                    print(message.get("result", ""))


asyncio.run(main())
```

All messages in the async iterable must be known upfront. The SDK sends them
sequentially to the CLI but cannot feed a prior response back into the generator.
If you need conversational turn-taking, manage each turn as a separate `query()`
call.

## Runtime Controls

The returned `Query` object can control the running CLI process:

```python
import asyncio

from qwen_code_sdk import is_sdk_result_message, query


async def main():
    async with query(
        "Inspect this repository and explain the test layout.",
        {
            "cwd": "/path/to/project",
            "path_to_qwen_executable": "qwen",
        },
    ) as result:
        commands = await result.supported_commands()
        print(commands)

        await result.set_permission_mode("plan")
        await result.set_model("qwen-plus")

        async for message in result:
            if is_sdk_result_message(message):
                if message.get("is_error"):
                    error = message.get("error") or {}
                    print(f"Error: {error.get('message', 'Unknown error')}")
                else:
                    print(message.get("result", ""))


asyncio.run(main())
```

Use `interrupt()` to cancel the current operation, `close()` to clean up the
underlying process, and `get_session_id()` to persist a session id for later.

## Session Resume

```python
import asyncio

from qwen_code_sdk import is_sdk_result_message, query


async def main():
    # Resume a known session by its id.
    async with query(
        "Continue from this session.",
        {
            "path_to_qwen_executable": "qwen",
            "resume": "123e4567-e89b-12d3-a456-426614174000",
        },
    ) as known:
        async for message in known:
            if is_sdk_result_message(message):
                if message.get("is_error"):
                    error = message.get("error") or {}
                    print(f"Error: {error.get('message', 'Unknown error')}")
                else:
                    print(message.get("result", ""))


asyncio.run(main())
```

To continue the latest session instead:

```python
import asyncio

from qwen_code_sdk import is_sdk_result_message, query


async def main():
    async with query(
        "Continue the latest session.",
        {
            "path_to_qwen_executable": "qwen",
            "continue_session": True,
        },
    ) as latest:
        async for message in latest:
            if is_sdk_result_message(message):
                if message.get("is_error"):
                    error = message.get("error") or {}
                    print(f"Error: {error.get('message', 'Unknown error')}")
                else:
                    print(message.get("result", ""))


asyncio.run(main())
```

`resume` is useful when your application stores session ids. `continue_session`
delegates the selection of the latest session to the CLI.

## Error Model

- `ValidationError`: invalid options, invalid UUIDs, unsupported combinations
- `ControlRequestTimeoutError`: initialize, interrupt, or other control request
  timed out
- `ProcessExitError`: CLI exited non-zero
- `AbortError`: control request or session was cancelled

```python
from qwen_code_sdk import (
    ProcessExitError,
    ValidationError,
    is_sdk_result_message,
    query_sync,
)

try:
    with query_sync("Say hello", {"path_to_qwen_executable": "qwen"}) as result:
        for message in result:
            if is_sdk_result_message(message):
                if message.get("is_error"):
                    error = message.get("error") or {}
                    print(f"Error: {error.get('message', 'Unknown error')}")
                else:
                    print(message.get("result", ""))
except ValidationError as exc:
    print(f"Invalid SDK options: {exc}")
except ProcessExitError as exc:
    print(f"qwen exited with {exc.exit_code}: {exc}")
```

## Troubleshooting

If the SDK cannot start the CLI:

- Verify `qwen --version` works in the target environment
- Pass `path_to_qwen_executable` if your shell uses `nvm`, `pyenv`, or other
  non-standard PATH setup
- Use `debug=True` or `stderr=print` to surface CLI stderr while debugging

If session control calls time out:

- Check that the target `qwen` version supports `--input-format stream-json`
- Increase `timeout.control_request`
- Verify that no wrapper script is swallowing stdout/stderr

## Repository Integration

Repository-level helper commands:

- `npm run test:sdk:python`
- `npm run lint:sdk:python`
- `npm run typecheck:sdk:python`
- `npm run smoke:sdk:python -- --qwen qwen`

## Real E2E Smoke

For a real runtime check (actual `qwen` process + real model call), run from
the repository root. The npm helper uses `python3`, so ensure it resolves to a
Python `>=3.10` interpreter:

```bash
npm run smoke:sdk:python -- --qwen qwen
```

This script runs:

- async single-turn query
- async control flow (`supported_commands`, permission mode updates)
- sync `query_sync` query

It prints JSON and returns non-zero on failure.

---

### Developers Sdk Typescript (`docs/developers/sdk-typescript.md`)

# Typescript SDK

## @qwen-code/sdk

A minimum experimental TypeScript SDK for programmatic access to Qwen Code.

Feel free to submit a feature request/issue/PR.

## Installation

```bash
npm install @qwen-code/sdk
```

## Requirements

- Node.js >= 22.0.0
- [Qwen Code](https://github.com/QwenLM/qwen-code) >= 0.4.0 (stable). The SDK uses its bundled CLI by default; set `pathToQwenExecutable` only when you need to run a custom `qwen` binary or CLI bundle.

## Quick Start

```typescript
import { query } from '@qwen-code/sdk';

// Single-turn query
const result = query({
  prompt: 'What files are in the current directory?',
  options: {
    cwd: '/path/to/project',
  },
});

// Iterate over messages
for await (const message of result) {
  if (message.type === 'assistant') {
    console.log('Assistant:', message.message.content);
  } else if (message.type === 'result') {
    console.log('Result:', message.result);
  }
}
```

## API Reference

### `query(config)`

Creates a new query session with the Qwen Code.

#### Parameters

- `prompt`: `string | AsyncIterable<SDKUserMessage>` - The prompt to send. Use a string for single-turn queries or an async iterable for multi-turn conversations.
- `options`: `QueryOptions` - Configuration options for the query session.

#### QueryOptions

| Option                   | Type                                                                 | Default          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------ | -------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cwd`                    | `string`                                                             | `process.cwd()`  | The working directory for the query session. Determines the context in which file operations and commands are executed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `model`                  | `string`                                                             | -                | The AI model to use (e.g., `'qwen-max'`, `'qwen-plus'`, `'qwen-turbo'`). Takes precedence over `OPENAI_MODEL` and `QWEN_MODEL` environment variables.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `pathToQwenExecutable`   | `string`                                                             | Bundled CLI      | Path to the Qwen Code executable. Supports multiple formats: `'qwen'` (native binary from PATH), `'/path/to/qwen'` (explicit path), `'/path/to/cli.js'` (Node.js bundle), `'node:/path/to/cli.js'` (force Node.js runtime), `'bun:/path/to/cli.js'` (force Bun runtime). If not provided, the SDK uses the bundled CLI included with the package.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `permissionMode`         | `'default' \| 'plan' \| 'auto-edit' \| 'auto' \| 'yolo'`             | `'default'`      | Permission mode controlling tool execution approval. See [Permission Modes](#permission-modes) for details.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `canUseTool`             | `CanUseTool`                                                         | -                | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `env`                    | `Record<string, string>`                                             | -                | Environment variables to pass to the Qwen Code process. Merged with the current process environment.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `systemPrompt`           | `string \| QuerySystemPromptPreset`                                  | -                | System prompt configuration for the main session. Use a string to fully override the built-in Qwen Code system prompt, or a preset object to keep the built-in prompt and append extra instructions.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `mcpServers`             | `Record<string, McpServerConfig>`                                    | -                | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like `command`, `args`, `url`, `httpUrl`, etc. SDK servers use `{ type: 'sdk', name: string, instance: Server }`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `abortController`        | `AbortController`                                                    | -                | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `debug`                  | `boolean`                                                            | `false`          | Enable debug mode for verbose logging from the CLI process.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `maxSessionTurns`        | `number`                                                             | `-1` (unlimited) | Maximum number of conversation turns before the session automatically terminates. Must be an integer. A turn consists of a user message and an assistant response.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `coreTools`              | `string[]`                                                           | -                | Uses the legacy `coreTools` / CLI `--core-tools` allowlist semantics. If specified, only matching core tools are registered for the session. This is the only allowlist-style option that restricts built-in tool registration; a whole-tool `permissions.deny` / `excludeTools` rule (and `tools.disabled` in settings.json) also removes a tool from the registry. `permissions.allow` in settings.json is pure auto-approval and never removes, demotes, or hides a tool (#10075). To keep a tool's schema out of the initial model request, use `tools.eager` in settings.json (requires restart, #9827) — `tool_search`, `tool_call`, `structured_output`, plan-mode lifecycle tools, `task_stop`, `mcp__*` and `computer_use__*` tools are exempt from that allowlist and keep their normal loading; tools demoted this way stay registered and reachable through `tool_search` + `tool_call` while both bridge tools are registered — when either is unregistered (`tools.toolSearch.enabled: false` denies both; a `tool_search` or `tool_call` deny rule, or a `tools.disabled` entry removes one) the demoted tools that remain hidden are not offered to the model and cannot be reached through the bridge for that session, and a warning is written to the CLI process's stderr (SDK forwards it only with piped stderr and effective `debug` logging; an explicit `logLevel` always wins); they stay registered, so a direct call by their own name is still evaluated and approved normally — except tools also listed in `tools.visible`, which are declared upfront, and sessions whose live history contains a direct call to a still-hidden demoted tool, which any tool-set refresh (resume, MCP discovery, the first plan-mode entry in a session, a subagent definition change) re-declares.; to remove a tool entirely, use a whole-tool `excludeTools` / `permissions.deny` rule — a rule with a specifier (such as `'Bash(rm *)'`) only denies matching invocations at runtime. MCP tools are exempt from deny-based removal: hide them with the per-server `excludeTools` / `tools.disabled` filters instead (deny still blocks their calls at runtime). Example: `['read_file', 'edit', 'run_shell_command']`. |
| `excludeTools`           | `string[]`                                                           | -                | Equivalent to `permissions.deny` in settings.json. Excluded tools return a permission error immediately. Takes highest priority over all other permission settings. Supports tool name aliases and pattern matching: tool name (`'write_file'`), shell command prefix (`'Bash(rm *)'`), or path patterns (`'Read(.env)'`, `'Edit(/src/**)'`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `allowedTools`           | `string[]`                                                           | -                | Equivalent to `permissions.allow` in settings.json for auto-approval. Matching tools bypass `canUseTool` callback and execute automatically. Only applies when tool requires confirmation. Like `permissions.allow`, this is pure auto-approval and never affects which tools are registered or which schemas are sent (#10075). Supports same pattern matching as `excludeTools`. Example: `['Bash(git status)', 'Bash(npm test)']`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `authType`               | `'openai' \| 'anthropic' \| 'qwen-oauth' \| 'gemini' \| 'vertex-ai'` | -                | Authentication type for the AI service. When provided, the SDK forwards it to the CLI as `--auth-type`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `agents`                 | `SubagentConfig[]`                                                   | -                | Configuration for subagents that can be invoked during the session. Subagents are specialized AI agents for specific tasks or domains.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `includePartialMessages` | `boolean`                                                            | `false`          | When `true`, the SDK emits incomplete messages as they are being generated, allowing real-time streaming of the AI's response.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `resume`                 | `string`                                                             | -                | Resume a previous session by providing its session ID. Equivalent to CLI's `--resume` flag.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `sessionId`              | `string`                                                             | -                | Specify a session ID for the new session. Ensures SDK and CLI use the same ID without resuming history. Equivalent to CLI's `--session-id` flag.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

> [!note]
> For `coreTools`, aliases like `Read`, `Edit`, and `Bash` also work, but invocation specifiers such as `Bash(git *)` are stripped. `coreTools` restricts tool registration, not invocation patterns.

#### Empty core-tool allowlists

An omitted or empty `coreTools` list is treated as unspecified; `coreTools: []` does **not** create a tool-free session. A non-empty list restricts core tools, not every tool category: MCP and other non-core tools have separate registration and permission rules. Use `excludeTools` to exclude named tools explicitly. `allowedTools` controls approval, not which tools are registered.

### Timeouts

The SDK enforces the following default timeouts:

| Timeout          | Default  | Description                                                                                                                                       |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `canUseTool`     | 1 minute | Maximum time for `canUseTool` callback to respond. If exceeded, the tool request is auto-denied.                                                  |
| `mcpRequest`     | 1 minute | Maximum time for SDK MCP tool calls to complete.                                                                                                  |
| `controlRequest` | 1 minute | Maximum time for control operations like `initialize()`, `setModel()`, `setPermissionMode()`, `getContextUsage()`, and `interrupt()` to complete. |
| `streamClose`    | 1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers.                             |

You can customize these timeouts via the `timeout` option:

```typescript
import { query } from '@qwen-code/sdk';

const q = query({
  prompt: 'Your prompt',
  options: {
    timeout: {
      canUseTool: 60000, // 60 seconds for permission callback
      mcpRequest: 600000, // 10 minutes for MCP tool calls
      controlRequest: 60000, // 60 seconds for control requests
      streamClose: 15000, // 15 seconds for stream close wait
    },
  },
});
```

### Message Types

The SDK provides type guards to identify different message types:

```typescript
import {
  isSDKUserMessage,
  isSDKAssistantMessage,
  isSDKSystemMessage,
  isSDKResultMessage,
  isSDKPartialAssistantMessage,
} from '@qwen-code/sdk';

for await (const message of result) {
  if (isSDKAssistantMessage(message)) {
    // Handle assistant message
  } else if (isSDKResultMessage(message)) {
    // Handle result message
  }
}
```

### Query Instance Methods

The `Query` instance returned by `query()` provides several methods:

```typescript
const q = query({ prompt: 'Hello', options: {} });

// Get session ID
const sessionId = q.getSessionId();

// Check if closed
const closed = q.isClosed();

// Interrupt the current operation
await q.interrupt();

// Change permission mode mid-session
await q.setPermissionMode('yolo');

// Change model mid-session
await q.setModel('qwen-max');

// Get context window usage breakdown (token counts per category)
const usage = await q.getContextUsage();
// Pass true to hint that per-item details should be displayed
const detail = await q.getContextUsage(true);

// Close the session
await q.close();
```

`interrupt()` cancels only the active turn. For a multi-turn query created with
an async iterable prompt, the query and its input stream remain open, so later
messages from the iterable are processed normally. Use `close()` or abort the
configured `AbortController` when you want to end the entire session.

## Daemon caller-supplied session IDs

`DaemonClient.createOrAttachSession` accepts an optional `sessionId` for callers that must persist an identity before session creation:

```typescript
import { DaemonClient } from '@qwen-code/sdk';

const daemon = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170' });
const session = await daemon.createOrAttachSession({
  workspaceCwd: '/path/to/project',
  sessionId: '550E8400-E29B-41D4-A716-446655440000',
});

console.log(session.sessionId); // 550e8400-e29b-41d4-a716-446655440000
```

The SDK requires the daemon's `session_id_override` capability before sending the mutation. REST mode serializes `sessionId` directly; an active ACP adapter maps it to `session/new._meta["qwen-code/sessionId"]`. The SDK verifies the success response and throws `DaemonSessionIdProtocolError` if the daemon returns a different ID.

This option always creates a new thread session and is not an idempotent attach. If the create outcome is ambiguous, use the known ID with load or resume. Omitting the option preserves the existing create-or-attach behavior.

## Daemon startup model and reasoning selection

`DaemonClient.createOrAttachSession` and `createStandaloneSession` accept `startupConfig: { modelServiceId, reasoningEffort? }` and preflight `session_startup_config` before creation. The model is required; omit reasoning for model-only selection, including models with no reasoning control. An explicit effort is validated against the model. The SDK checks `modelApplied: true` and the canonical `startupConfigApplied` response; model-only confirmation omits reasoning fields.

Startup selection does not save shared defaults or impose a lifetime pin. Definite selection rejection fails creation; uncertain standalone creation retains its existing recovery result. A successful response whose startup confirmation is missing or mismatched is rejected directly and does not trigger standalone recovery/adoption. See the [protocol](./qwen-serve-protocol.md#capabilities).

## Talking to running sessions

`@qwen-code/sdk/peer` lets a program that is not a Qwen Code session join the
sessions running as the same user on the same machine — a voice front-end, a
relay, a build watcher. The program shows up in `qwen sessions ps`, and in the
`list_agents` of every session that takes part in cross-session messaging — its
`agents.crossSessionMessaging` setting is on, and it was not started with
`--bare` or `--safe-mode` — which is also what lets those sessions message it
by name with `send_message`. It can message them back. It runs on Node only and
needs nothing beyond Node itself.

```typescript
import { PeerEndpoint } from '@qwen-code/sdk/peer';

const endpoint = await PeerEndpoint.start({
  name: 'voice-bridge',
  onMessage: (message) =>
    console.log(`${message.fromName}: ${message.content}`),
});

const [session] = await endpoint.list();
if (session) {
  const sent = await endpoint.send({
    to: session.address,
    content: 'What are you working on?',
  });
  if (sent.kind === 'sent') {
    const receipt = await endpoint.awaitReceipt(sent.msgId, { final: true });
    console.log(receipt?.status); // delivered, denied, refused, ...
  }
}

await endpoint.close();
```

A message like that is held for the session's user to review. To direct a
session without that review, mint a controller token with
`qwen sessions controllers add --label voice-bridge`, give it to the endpoint,
and mark the sends that should present it:

```typescript
const endpoint = await PeerEndpoint.start({
  name: 'voice-bridge',
  controllerToken: process.env['QWEN_CONTROLLER_TOKEN'],
});
await endpoint.send({
  to: 'my-app-3f',
  content: 'run the tests',
  controller: true,
});
```

Things to know:

- A session has an inbox only while its `agents.crossSessionMessaging` setting
  is on, which it is by default, and it was not started with `--bare` or
  `--safe-mode` — those turn messaging off whatever the setting says. A session
  that turned it off does not appear in `list()`, and its own `list_agents` and
  `send_message` cannot see or reach the program either. `qwen sessions ps`
  lists the program regardless.
- A message is delivered without review in exactly two cases: the send
  presents a controller token (`controller: true`), or its `fromMode` names the
  receiving session's own review class. `fromMode` is a claim nothing
  authenticates, so a program that is not a coding session should leave it
  out. Nothing in the record — not `kind`, not `name` — buys delivery. The
  receiving session's `agents.crossSessionInbound` setting outranks both:
  `hold` or `refuse` there wins over a controller token.
- Mark only the sends meant to direct a session. Addresses are resolved from
  records any program running as you can write, so a controller send presents
  the token to whichever process's record answers to that address. A
  controller send to another peer endpoint is dropped unread, because an
  endpoint's inbox accepts only its own token.
- The endpoint's inbox applies none of the protections a Qwen Code session
  applies to its own: no rate limit, no holds, and no duplicate window beyond
  the last 200 messages it answered. Every message is answered `delivered` and
  handed to `onMessage` as it arrives, so apply your own limits there if you
  need them. Without `onMessage`, every message is answered `refused`.
- Call `close()` before exiting, including from your own signal handlers. A
  process killed without closing leaves its record behind until a Qwen Code
  session lists the directory and sees the process is gone.
- UNIX domain sockets only: Windows is not supported yet.

The record schema, wire format and receipt states are documented in
[Cross-Session Protocol](../users/features/cross-session-protocol.md).

## Permission Modes

The SDK supports different permission modes for controlling tool execution:

- **`default`**: Write tools are denied unless approved via `canUseTool` callback or in `allowedTools`. Read-only tools execute without confirmation.
- **`plan`**: Blocks all write tools, instructing AI to present a plan first.
- **`auto-edit`**: Auto-approve edit tools (`edit`, `write_file`, `notebook_edit`) while other tools require confirmation.
- **`auto`**: Uses the built-in classifier to auto-approve safe tool calls and block risky ones, with manual-approval fallback after repeated policy blocks or classifier outages.
- **`yolo`**: All tools execute automatically without confirmation.

### Permission Priority Chain

Decision priority (highest first): `deny` > `ask` > `allow` > _(default/interactive mode)_

The first matching rule wins.

1. `excludeTools` / `permissions.deny` - Blocks tools completely (returns permission error)
2. `permissions.ask` - Always requires user confirmation
3. `permissionMode: 'plan'` - Blocks all non-read-only tools
4. `permissionMode: 'yolo'` - Auto-approves all tools
5. `allowedTools` / `permissions.allow` - Auto-approves matching tools
6. `permissionMode: 'auto'` - Classifier-mediated approval for remaining tools
7. `canUseTool` callback - Custom approval logic (if provided, not called for allowed tools)
8. Default behavior - Auto-deny in SDK mode (write tools require explicit approval)

## Examples

### Multi-turn Conversation

```typescript
import { query, type SDKUserMessage } from '@qwen-code/sdk';

async function* generateMessages(): AsyncIterable<SDKUserMessage> {
  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Create a hello.txt file' },
    parent_tool_use_id: null,
  };

  // Wait for some condition or user input
  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Now read the file back' },
    parent_tool_use_id: null,
  };
}

const result = query({
  prompt: generateMessages(),
  options: {
    permissionMode: 'auto-edit',
  },
});

for await (const message of result) {
  console.log(message);
}
```

### Custom Permission Handler

```typescript
import { query, type CanUseTool } from '@qwen-code/sdk';

const canUseTool: CanUseTool = async (toolName, input, { signal }) => {
  // Allow all read operations
  if (toolName.startsWith('read_')) {
    return { behavior: 'allow', updatedInput: input };
  }

  // Prompt user for write operations (in a real app)
  const userApproved = await promptUser(`Allow ${toolName}?`);

  if (userApproved) {
    return { behavior: 'allow', updatedInput: input };
  }

  return { behavior: 'deny', message: 'User denied the operation' };
};

const result = query({
  prompt: 'Create a new file',
  options: {
    canUseTool,
  },
});
```

### With External MCP Servers

```typescript
import { query } from '@qwen-code/sdk';

const result = query({
  prompt: 'Use the custom tool from my MCP server',
  options: {
    mcpServers: {
      'my-server': {
        command: 'node',
        args: ['path/to/mcp-server.js'],
        env: { PORT: '3000' },
      },
    },
  },
});
```

### Override the System Prompt

```typescript
import { query } from '@qwen-code/sdk';

const result = query({
  prompt: 'Say hello in one sentence.',
  options: {
    systemPrompt: 'You are a terse assistant. Answer in exactly one sentence.',
  },
});
```

### Append to the Built-in System Prompt

```typescript
import { query } from '@qwen-code/sdk';

const result = query({
  prompt: 'Review the current directory.',
  options: {
    systemPrompt: {
      type: 'preset',
      preset: 'qwen_code',
      append: 'Be terse and focus on concrete findings.',
    },
  },
});
```

### With SDK-Embedded MCP Servers

The SDK provides `tool` and `createSdkMcpServer` to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process.

#### `tool(name, description, inputSchema, handler)`

Creates a tool definition with Zod schema type inference.

| Parameter     | Type                               | Description                                                              |
| ------------- | ---------------------------------- | ------------------------------------------------------------------------ |
| `name`        | `string`                           | Tool name (1-64 chars, starts with letter, alphanumeric and underscores) |
| `description` | `string`                           | Human-readable description of what the tool does                         |
| `inputSchema` | `ZodRawShape`                      | Zod schema object defining the tool's input parameters                   |
| `handler`     | `(args, extra) => Promise<Result>` | Async function that executes the tool and returns MCP content blocks     |

The handler must return a `CallToolResult` object with the following structure:

```typescript
{
  content: Array<
    | { type: 'text'; text: string }
    | { type: 'image'; data: string; mimeType: string }
    | { type: 'resource'; uri: string; mimeType?: string; text?: string }
  >;
  isError?: boolean;
}
```

#### `createSdkMcpServer(options)`

Creates an SDK-embedded MCP server instance.

| Option    | Type                     | Default   | Description                          |
| --------- | ------------------------ | --------- | ------------------------------------ |
| `name`    | `string`                 | Required  | Unique name for the MCP server       |
| `version` | `string`                 | `'1.0.0'` | Server version                       |
| `tools`   | `SdkMcpToolDefinition[]` | -         | Array of tools created with `tool()` |

Returns a `McpSdkServerConfigWithInstance` object that can be passed directly to the `mcpServers` option.

#### Example

```typescript
import { z } from 'zod';
import { query, tool, createSdkMcpServer } from '@qwen-code/sdk';

// Define a tool with Zod schema
const calculatorTool = tool(
  'calculate_sum',
  'Add two numbers',
  { a: z.number(), b: z.number() },
  async (args) => ({
    content: [{ type: 'text', text: String(args.a + args.b) }],
  }),
);

// Create the MCP server
const server = createSdkMcpServer({
  name: 'calculator',
  tools: [calculatorTool],
});

// Use the server in a query
const result = query({
  prompt: 'What is 42 + 17?',
  options: {
    permissionMode: 'yolo',
    mcpServers: {
      calculator: server,
    },
  },
});

for await (const message of result) {
  console.log(message);
}
```

### Abort a Query

```typescript
import { query, isAbortError } from '@qwen-code/sdk';

const abortController = new AbortController();

const result = query({
  prompt: 'Long running task...',
  options: {
    abortController,
  },
});

// Abort after 5 seconds
setTimeout(() => abortController.abort(), 5000);

try {
  for await (const message of result) {
    console.log(message);
  }
} catch (error) {
  if (isAbortError(error)) {
    console.log('Query was aborted');
  } else {
    throw error;
  }
}
```

## Error Handling

The SDK provides an `AbortError` class for handling aborted queries:

```typescript
import { AbortError, isAbortError } from '@qwen-code/sdk';

try {
  // ... query operations
} catch (error) {
  if (isAbortError(error)) {
    // Handle abort
  } else {
    // Handle other errors
  }
}
```

## Human-origin workflow hints

Streaming `SDKUserMessage` input supports per-message `origin: { kind: 'human' }` for original human submissions. Messages without the declaration and string convenience prompts remain ordinary chat without keyword activation. See the [workflow intent contract](../users/workflow-intent.md) ([简体中文](../users/workflow-intent.zh-CN.md)) for examples and exclusions.

---

### Plans 2026 03 22 Agent Tool Display Design (`docs/plans/2026-03-22-agent-tool-display-design.md`)

# Agent Tool Display Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add a dedicated VSCode/web UI display for Agent tool executions so subagent progress, summaries, and failures render from structured `rawOutput` instead of falling back to the generic tool card.

**Architecture:** Preserve ACP `rawOutput` through the VSCode session/update pipeline into `ToolCallData`, then let the shared web UI router detect `task_execution` payloads and render a dedicated `AgentToolCall` component. Keep the change shared in `packages/webui` so VSCode and `ChatViewer` stay aligned.

**Tech Stack:** TypeScript, React, Vitest, shared `@qwen-code/webui` tool-call components.

### Task 1: Lock in the failing data-flow behavior

**Files:**

- Modify: `packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.test.ts`
- Create: `packages/vscode-ide-companion/src/webview/hooks/useToolCalls.test.tsx`

**Step 1: Write the failing tests**

- Add a session handler test asserting `tool_call_update` forwards `rawOutput` when ACP sends a `task_execution` payload.
- Add a hook test asserting `useToolCalls` stores and updates `rawOutput` for an agent tool call.

**Step 2: Run test to verify it fails**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run qwenSessionUpdateHandler.test.ts useToolCalls.test.tsx`

Expected: failures because `rawOutput` is not preserved in the current handler/hook pipeline.

### Task 2: Lock in the failing renderer behavior

**Files:**

- Create: `packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.test.tsx`

**Step 1: Write the failing test**

- Render the routed tool call with `kind: 'other'` plus `rawOutput.type === 'task_execution'`.
- Assert the task description, active child tool, summary, and failure reason render from a dedicated agent display instead of generic text output.

**Step 2: Run test to verify it fails**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.test.tsx`

Expected: failure because the router only keys off `kind` and no dedicated agent component exists.

### Task 3: Preserve structured agent output end-to-end

**Files:**

- Modify: `packages/vscode-ide-companion/src/types/chatTypes.ts`
- Modify: `packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts`
- Modify: `packages/vscode-ide-companion/src/webview/hooks/useToolCalls.ts`
- Modify: `packages/webui/src/components/toolcalls/shared/types.ts`

**Step 1: Implement the minimal data model changes**

- Add optional `rawOutput` to the VSCode session/webview tool-call types.
- Forward `rawOutput` in `QwenSessionUpdateHandler`.
- Store/merge `rawOutput` in `useToolCalls`.
- Expose `rawOutput` in shared web UI tool-call data types.

**Step 2: Run the focused tests**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run qwenSessionUpdateHandler.test.ts useToolCalls.test.tsx`

Expected: pass.

### Task 4: Add the shared agent tool-call UI

**Files:**

- Create: `packages/webui/src/components/toolcalls/AgentToolCall.tsx`
- Modify: `packages/webui/src/components/toolcalls/index.ts`
- Modify: `packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx`
- Modify: `packages/webui/src/components/ChatViewer/ChatViewer.tsx`

**Step 1: Implement the minimal renderer**

- Add a guard for `rawOutput.type === 'task_execution'`.
- Render task description as the header.
- Show agent name + status, currently running child tools, completion summary, and failure/cancel reason.
- Keep the layout compatible with multiple parallel agent cards by rendering each tool call independently.

**Step 2: Run the focused renderer test**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.test.tsx`

Expected: pass.

### Task 5: Verify the integrated surface

**Files:**

- Modify: `packages/webui/src/index.ts`

**Step 1: Export the new shared component if needed**

- Re-export any new component/types needed by VSCode or `ChatViewer`.

**Step 2: Run package verification**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run qwenSessionUpdateHandler.test.ts useToolCalls.test.tsx packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.test.tsx`
Run: `npm run check-types --workspace=packages/vscode-ide-companion`
Run: `npm run typecheck --workspace=packages/webui`

Expected: all targeted tests and typechecks pass.

---

### Users Common Workflow (`docs/users/common-workflow.md`)

# Common workflows

> Learn about common workflows with Qwen Code.

Each task in this document includes clear instructions, example commands, and best practices to help you get the most from Qwen Code.

## Understand new codebases

### Get a quick codebase overview

Suppose you've just joined a new project and need to understand its structure quickly.

**1. Navigate to the project root directory**

```bash
cd /path/to/project
```

**2. Start Qwen Code**

```bash
qwen
```

**3. Ask for a high-level overview**

```
give me an overview of this codebase
```

**4. Dive deeper into specific components**

```
explain the main architecture patterns used here
```

```
what are the key data models?
```

```
how is authentication handled?
```

> [!tip]
>
> - Start with broad questions, then narrow down to specific areas
> - Ask about coding conventions and patterns used in the project
> - Request a glossary of project-specific terms

### Find relevant code

Suppose you need to locate code related to a specific feature or functionality.

**1. Ask Qwen Code to find relevant files**

```
find the files that handle user authentication
```

**2. Get context on how components interact**

```
how do these authentication files work together?
```

**3. Understand the execution flow**

```
trace the login process from front-end to database
```

> [!tip]
>
> - Be specific about what you're looking for
> - Use domain language from the project

## Fix bugs efficiently

Suppose you've encountered an error message and need to find and fix its source.

**1. Share the error with Qwen Code**

```
I'm seeing an error when I run npm test
```

**2. Ask for fix recommendations**

```
suggest a few ways to fix the @ts-ignore in user.ts
```

**3. Apply the fix**

```
update user.tsto add the null check you suggested
```

> [!tip]
>
> - Tell Qwen Code the command to reproduce the issue and get a stack trace
> - Mention any steps to reproduce the error
> - Let Qwen Code know if the error is intermittent or consistent

## Refactor code

Suppose you need to update old code to use modern patterns and practices.

**1. Identify legacy code for refactoring**

```
find deprecated API usage in our codebase
```

**2. Get refactoring recommendations**

```
suggest how to refactor utils.js to use modern JavaScript features
```

**3. Apply the changes safely**

```
refactor utils.js to use ES 2024 features while maintaining the same behavior
```

**4. Verify the refactoring**

```
run tests for the refactored code
```

> [!tip]
>
> - Ask Qwen Code to explain the benefits of the modern approach
> - Request that changes maintain backward compatibility when needed
> - Do refactoring in small, testable increments

## Use specialized subagents

Suppose you want to use specialized AI subagents to handle specific tasks more effectively.

**1. View available subagents**

```
/agents
```

This shows all available subagents and lets you create new ones.

**2. Use subagents automatically**

Qwen Code automatically delegates appropriate tasks to specialized subagents:

```
review my recent code changes for security issues
```

```
run all tests and fix any failures
```

**3. Explicitly request specific subagents**

```
use the code-reviewer subagent to check the auth module
```

```
have the debugger subagent investigate why users can't log in
```

**4. Create custom subagents for your workflow**

```
/agents
```

Then select "create" and follow the prompts to define:

- A unique identifier that describes the subagent's purpose (for example, `code-reviewer`, `api-designer`).
- When Qwen Code should use this agent
- Which tools it can access
- A system prompt describing the agent's role and behavior

> [!tip]
>
> - Create project-specific subagents in `.qwen/agents/` for team sharing
> - Use descriptive `description` fields to enable automatic delegation
> - Limit tool access to what each subagent actually needs
> - Know more about [Sub Agents](./features/sub-agents)
> - Know more about [Approval Mode](./features/approval-mode)

## Work with tests

Suppose you need to add tests for uncovered code.

**1. Identify untested code**

```
find functions in NotificationsService.swift that are not covered by tests
```

**2. Generate test scaffolding**

```
add tests for the notification service
```

**3. Add meaningful test cases**

```
add test cases for edge conditions in the notification service
```

**4. Run and verify tests**

```
run the new tests and fix any failures
```

Qwen Code can generate tests that follow your project's existing patterns and conventions. When asking for tests, be specific about what behavior you want to verify. Qwen Code examines your existing test files to match the style, frameworks, and assertion patterns already in use.

For comprehensive coverage, ask Qwen Code to identify edge cases you might have missed. Qwen Code can analyze your code paths and suggest tests for error conditions, boundary values, and unexpected inputs that are easy to overlook.

## Create pull requests

Suppose you need to create a well-documented pull request for your changes.

**1. Summarize your changes**

```
summarize the changes I've made to the authentication module
```

**2. Generate a pull request with Qwen Code**

```
create a pr
```

**3. Review and refine**

```
enhance the PR description with more context about the security improvements
```

**4. Add testing details**

```
add information about how these changes were tested
```

> [!tip]
>
> - Ask Qwen Code directly to make a PR for you
> - Review Qwen Code's generated PR before submitting
> - Ask Qwen Code to highlight potential risks or considerations

## Handle documentation

Suppose you need to add or update documentation for your code.

**1. Identify undocumented code**

```
find functions without proper JSDoc comments in the auth module
```

**2. Generate documentation**

```
add JSDoc comments to the undocumented functions in auth.js
```

**3. Review and enhance**

```
improve the generated documentation with more context and examples
```

**4. Verify documentation**

```
check if the documentation follows our project standards
```

> [!tip]
>
> - Specify the documentation style you want (JSDoc, docstrings, etc.)
> - Ask for examples in the documentation
> - Request documentation for public APIs, interfaces, and complex logic

## Reference files and directories

Use `@` to quickly include files or directories without waiting for Qwen Code to read them.

**1. Reference a single file**

```
Explain the logic in @src/utils/auth.js
```

This includes the full content of the file in the conversation.

**2. Reference a directory**

```
What's the structure of @src/components?
```

This provides a directory listing with file information.

**3. Reference MCP resources**

```
Show me the data from @github: repos/owner/repo/issues
```

This fetches data from connected MCP servers using the format @server: resource. See [MCP](./features/mcp) for details.

> [!tip]
>
> - File paths can be relative or absolute
> - @ file references add `QWEN.md` in the file's directory and parent directories to context
> - Directory references show file listings, not contents
> - You can reference multiple files in a single message (for example, "`@file 1.js` and `@file 2.js`")

## Resume previous conversations

Suppose you've been working on a task with Qwen Code and need to continue where you left off in a later session.

Qwen Code provides two options for resuming previous conversations:

- `--continue` to automatically continue the most recent conversation
- `--resume` to display a conversation picker

**1. Continue the most recent conversation**

```bash
qwen --continue
```

This immediately resumes your most recent conversation without any prompts.

**2. Continue in non-interactive mode**

```bash
qwen --continue -p "Continue with my task"
```

Use `-p` (or `--prompt`) with `--continue` to resume the most recent conversation in non-interactive mode, perfect for scripts or automation.

**3. Show conversation picker**

```bash
qwen --resume
```

This displays an interactive conversation selector with a clean list view showing:

- Session summary (or initial prompt)
- Metadata: time elapsed, message count, and git branch

Use arrow keys to navigate and press Enter to select a conversation. Press Esc to exit.

> [!tip]
>
> - Conversation history is stored locally on your machine
> - Use `--continue` for quick access to your most recent conversation
> - Use `--resume` when you need to select a specific past conversation
> - When resuming, you'll see the entire conversation history before continuing
> - The resumed conversation starts with the same model and configuration as the original
>
> **How it works**:
>
> 1. **Conversation Storage**: All conversations are automatically saved locally with their full message history
> 2. **Message Deserialization**: When resuming, the entire message history is restored to maintain context
> 3. **Tool State**: Tool usage and results from the previous conversation are preserved
> 4. **Context Restoration**: The conversation resumes with all previous context intact
>
> **Examples**:
>
> ```bash
> # Continue most recent conversation
> qwen --continue
>
> # Continue most recent conversation with a specific prompt
> qwen --continue -p "Show me our progress"
>
> # Show conversation picker
> qwen --resume
>
> # Continue most recent conversation in non-interactive mode
> qwen --continue -p "Run the tests again"
> ```

## Run parallel Qwen Code sessions with Git worktrees

Suppose you need to work on multiple tasks simultaneously with complete code isolation between Qwen Code instances.

**1. Understand Git worktrees**

Git worktrees allow you to check out multiple branches from the same repository into separate directories. Each worktree has its own working directory with isolated files, while sharing the same Git history. Learn more in the [official Git worktree documentation](https://git-scm.com/docs/git-worktree).

**2. Create a new worktree**

```bash
# Create a new worktree with a new branch
git worktree add ../project-feature-a -b feature-a

# Or create a worktree with an existing branch
git worktree add ../project-bugfix bugfix-123
```

This creates a new directory with a separate working copy of your repository.

**3. Run Qwen Code in each worktree**

```bash
# Navigate to your worktree
cd ../project-feature-a

# Run Qwen Code in this isolated environment
qwen
```

**4. Run Qwen Code in another worktree**

```bash
cd ../project-bugfix
qwen
```

**5. Manage your worktrees**

```bash
# List all worktrees
git worktree list

# Remove a worktree when done
git worktree remove ../project-feature-a
```

> [!tip]
>
> - Each worktree has its own independent file state, making it perfect for parallel Qwen Code sessions
> - Changes made in one worktree won't affect others, preventing Qwen Code instances from interfering with each other
> - All worktrees share the same Git history and remote connections
> - For long-running tasks, you can have Qwen Code working in one worktree while you continue development in another
> - Use descriptive directory names to easily identify which task each worktree is for
> - Remember to initialize your development environment in each new worktree according to your project's setup. Depending on your stack, this might include:
>   - JavaScript projects: Running dependency installation (`npm install`, `yarn`)
>   - Python projects: Setting up virtual environments or installing with package managers
>   - Other languages: Following your project's standard setup process

## Use Qwen Code as a unix-style utility

### Add Qwen Code to your verification process

Suppose you want to use Qwen Code as a linter or code reviewer.

**Add Qwen Code to your build script:**

```json
// package.json
{
    ...
    "scripts": {
        ...
        "lint:Qwen Code": "qwen -p 'you are a linter. please look at the changes vs. main and report any issues related to typos. report the filename and line number on one line, and a description of the issue on the second line. do not return any other text.'"
    }
}
```

> [!tip]
>
> - Use Qwen Code for automated code review in your CI/CD pipeline
> - Customize the prompt to check for specific issues relevant to your project
> - Consider creating multiple scripts for different types of verification

### Pipe in, pipe out

Suppose you want to pipe data into Qwen Code, and get back data in a structured format.

**Pipe data through Qwen Code:**

```bash
cat build-error.txt | qwen -p 'concisely explain the root cause of this build error' > output.txt
```

> [!tip]
>
> - Use pipes to integrate Qwen-Code into existing shell scripts
> - Combine with other Unix tools for powerful workflows
> - Consider using --output-format for structured output

### Control output format

Suppose you need Qwen Code's output in a specific format, especially when integrating Qwen Code into scripts or other tools.

**1. Use text format (default)**

```bash
cat data.txt | qwen -p 'summarize this data' --output-format text > summary.txt
```

This outputs just Qwen Code's plain text response (default behavior).

**2. Use JSON format**

```bash
cat code.py | qwen -p 'analyze this code for bugs' --output-format json > analysis.json
```

This outputs a JSON array of messages with metadata including cost and duration.

**3. Use streaming JSON format**

```bash
cat log.txt | qwen -p 'parse this log file for errors' --output-format stream-json
```

This outputs a series of JSON objects in real-time as Qwen Code processes the request. Each message is a valid JSON object, but the entire output is not valid JSON if concatenated.

> [!tip]
>
> - Use `--output-format text` for simple integrations where you just need Qwen Code's response
> - Use `--output-format json` when you need the full conversation log
> - Use `--output-format stream-json` for real-time output of each conversation turn

## Ask Qwen Code about its capabilities

Qwen Code has built-in access to its documentation and can answer questions about its own features and limitations.

### Example questions

```
can Qwen Code create pull requests?
```

```
how does Qwen Code handle permissions?
```

```
what slash commands are available?
```

```
how do I use MCP with Qwen Code?
```

```
how do I configure Qwen Code for Amazon Bedrock?
```

```
what are the limitations of Qwen Code?
```

> [!note]
>
> Qwen Code provides documentation-based answers to these questions. For executable examples and hands-on demonstrations, refer to the specific workflow sections above.

> [!tip]
>
> - Qwen Code always has access to the latest Qwen Code documentation, regardless of the version you're using
> - Ask specific questions to get detailed answers
> - Qwen Code can explain complex features like MCP integration, enterprise configurations, and advanced workflows

## Reuse saved workflows in a monorepo

With [`tools.workflowsEnabled`](./configuration/settings.md) enabled, a local terminal, headless, or direct ACP session can use saved workflows from trusted parent directories in the same Git repository. For example, when you start Qwen Code in `repo/packages/app`, a workflow named `review` is selected in this order:

```text
repo/packages/app/.qwen/workflows/review.js
repo/packages/.qwen/workflows/review.js
repo/.qwen/workflows/review.js
~/.qwen/workflows/review.js
```

The nearest definition wins. Parent discovery stops at the closest Git root, including a worktree or submodule root. A directory outside a Git repository keeps its own project and user workflows. Extension workflows retain their qualified names, such as `gcp:review`.

Folder trust follows the operator's settings; workspace settings cannot enable or disable this ancestor check. When folder trust is enabled, the current target and each parent must be trusted by the saved folder-trust rules. Trusting only the current directory does not grant access to its parents; an unknown or denied parent stops the search toward the root. IDE trust alone does not grant parent access. Changes to the folder-trust file take effect on the next discovery or script lookup, so a previously listed command cannot use its cached path to read a parent whose trust was revoked. Refresh the command list or start a new session to see added or removed commands. When folder trust is disabled, local sessions allow parent discovery within the Git boundary. On POSIX systems, this disabled mode still excludes parents whose directory, `.qwen`, or workflow directory is writable by everyone. Windows permission bits do not describe ACLs, so this POSIX permission check does not apply there.

Parent workflows still require the usual workflow approval, which identifies the selected script. Content changes invalidate approvals pinned to the script's digest. Running a parent's workflow keeps the session's working directory, and saving to project scope writes to the current session's `.qwen/workflows/` directory.

Daemon-managed ACP children, Web Shell, hosted sessions, and restricted sessions do not opt into parent discovery. Parent directories and their workflow files must be ordinary directories and files; symbolic links cannot grant access to a parent workflow.

## Enforce evidence-based conclusions

When Qwen Code does high-stakes work — code review, incident troubleshooting, audits, or investigating production data — a confident but wrong conclusion costs more than no conclusion at all. You can encode your team's verification policy in QWEN.md (see [Memory](./features/memory.md)) so it applies to every session, without changing anyone's defaults.

The rules below are adapted from a production retrospective of a multi-agent deployment, where confident-but-wrong conclusions repeatedly traced back to the same failure modes: inferring state from code instead of querying it, treating one satisfied condition as proof, and repeating earlier statements as facts.

**1. Add a verification policy to your project QWEN.md**

Put this template in `QWEN.md` at your repository root and commit it, so the policy applies to the whole team:

```markdown
# Verification policy

Treat these rules as mandatory in investigations, troubleshooting, reviews, and any task that draws conclusions:

1. Verify before concluding. Back claims about data or system state with evidence from the authoritative source (database, API, logs, command output). Conclusions drawn from reading code alone must be labeled "unverified inference".
2. Verify the full chain. Enumerate the causal chain and check every link; one satisfied necessary condition does not prove a conclusion.
3. Treat conversation history as leads, not facts. Statements in earlier transcripts — including your own prior assertions — are leads. Re-verify them at the source before repeating them as facts.
4. Troubleshoot in order: your own recent changes first, then docs and known issues, and only then external dependencies.
5. 100% evidence rule in reviews. In review and audit tasks, report no conclusion without evidence; attach verifiable evidence (file:line, query result, log excerpt) to every claim.
```

**2. Choose the right scope**

See [Where to create QWEN.md](./features/memory.md#where-to-create-qwenmd) for which file applies to whom — commit the project-root `QWEN.md` copy so the policy covers the whole team.

> [!tip]
>
> - Keep the rules specific and actionable: "back every claim with a query result" works better than "be careful".
> - Keep the policy short — QWEN.md is followed more reliably when it is concise (see [Memory](./features/memory.md)).
> - Adapt the rules to your stack: name the actual query tools, dashboards, or log commands your team treats as authoritative sources.

## Suggest a workflow for one human turn

Use `ultracode` or the existing `workflow` keyword in a supported human submission to suggest the Workflow tool for that turn. See [workflow intent](workflow-intent.md) ([简体中文](workflow-intent.zh-CN.md)) for entry points, SDK opt-in, and settings.

---

### Users Integration Github Action (`docs/users/integration-github-action.md`)

# GitHub Actions：qwen-code-action

## Overview

`qwen-code-action` is a GitHub Action that integrates [Qwen Code] into your development workflow via the [Qwen Code CLI]. It acts both as an autonomous agent for critical routine coding tasks, and an on-demand collaborator you can quickly delegate work to.

Use it to perform GitHub pull request reviews, triage issues, perform code analysis and modification, and more using [Qwen Code] conversationally (e.g., `@qwencoder fix this issue`) directly inside your GitHub repositories.

## Features

- **Automation**: Trigger workflows based on events (e.g. issue opening) or schedules (e.g. nightly).
- **On-demand Collaboration**: Trigger workflows in issue and pull request
  comments by mentioning the [Qwen Code CLI](./features/commands) (e.g., `@qwencoder /review`).
- **Extensible with Tools**: Leverage [Qwen Code](../developers/tools/introduction.md) models' tool-calling capabilities to interact with other CLIs like the [GitHub CLI] (`gh`).
- **Customizable**: Use a `QWEN.md` file in your repository to provide
  project-specific instructions and context to [Qwen Code CLI](./features/commands).

## Quick Start

Get started with Qwen Code CLI in your repository in just a few minutes:

### 1. Get a Qwen API Key

Obtain your API key from [DashScope](https://help.aliyun.com/zh/model-studio/qwen-code) (Alibaba Cloud's AI platform)

### 2. Add it as a GitHub Secret

Store your API key as a secret named `QWEN_API_KEY` in your repository:

- Go to your repository's **Settings > Secrets and variables > Actions**
- Click **New repository secret**
- Name: `QWEN_API_KEY`, Value: your API key

### 3. Update your .gitignore

Add the following entries to your `.gitignore` file:

```gitignore
# qwen-code-cli settings
.qwen/

# GitHub App credentials
gha-creds-*.json
```

### 4. Choose a Workflow

You have two options to set up a workflow:

**Option A: Use setup command (Recommended)**

1. Start the Qwen Code CLI in your terminal:

   ```shell
   qwen
   ```

2. In Qwen Code CLI in your terminal, type:

   ```
   /setup-github
   ```

**Option B: Manually copy workflows**

1. Copy the pre-built workflows from the [`examples/workflows`](./common-workflow) directory to your repository's `.github/workflows` directory. Note: the `qwen-dispatch.yml` workflow must also be copied, which triggers the workflows to run.

### 5. Try it out

**Pull Request Review:**

- Open a pull request in your repository and wait for automatic review
- Comment `@qwencoder /review` on an existing pull request to manually trigger a review

**Issue Triage:**

- Open an issue and wait for automatic triage
- Comment `@qwencoder /triage` on existing issues to manually trigger triaging

**General AI Assistance:**

- In any issue or pull request, mention `@qwencoder` followed by your request
- Examples:
  - `@qwencoder explain this code change`
  - `@qwencoder suggest improvements for this function`
  - `@qwencoder help me debug this error`
  - `@qwencoder write unit tests for this component`

## Workflows

This action provides several pre-built workflows for different use cases. Each workflow is designed to be copied into your repository's `.github/workflows` directory and customized as needed.

### Qwen Code Dispatch

This workflow acts as a central dispatcher for Qwen Code CLI, routing requests to the appropriate workflow based on the triggering event and the command provided in the comment. For a detailed guide on how to set up the dispatch workflow, go to the [Qwen Code Dispatch workflow documentation](./common-workflow).

### Issue Triage

This action can be used to triage GitHub Issues automatically or on demand. For a working issue triage setup, see the [Qwen triage workflow](https://github.com/QwenLM/qwen-code/blob/main/.github/workflows/qwen-triage.yml).

### Pull Request Review

This action can be used to automatically review pull requests when they are opened. For a detailed guide on how to set up the pull request review system, go to the [GitHub PR Review workflow documentation](./common-workflow).

### Qwen Code CLI Assistant

This type of action can be used to invoke a general-purpose, conversational Qwen Code AI assistant within the pull requests and issues to perform a wide range of tasks. For a detailed guide on how to set up the general-purpose Qwen Code CLI workflow, go to the [Qwen Code Assistant workflow documentation](./common-workflow).

## Configuration

### Inputs



- <a name="__input_qwen_api_key"></a><a href="#user-content-__input_qwen_api_key"><code>qwen*api_key</code></a>: *(Optional)\_ The API key for the Qwen API.

- <a name="__input_qwen_cli_version"></a><a href="#user-content-__input_qwen_cli_version"><code>qwen*cli_version</code></a>: *(Optional, default: `latest`)\_ The version of the Qwen Code CLI to install. Can be "latest", "preview", "nightly", a specific version number, or a git branch, tag, or commit. For more information, see [Qwen Code CLI releases](https://github.com/QwenLM/qwen-code/releases).

- <a name="__input_qwen_debug"></a><a href="#user-content-__input_qwen_debug"><code>qwen*debug</code></a>: *(Optional)\_ Enable debug logging and output streaming.

- <a name="__input_qwen_model"></a><a href="#user-content-__input_qwen_model"><code>qwen*model</code></a>: *(Optional)\_ The model to use with Qwen Code.

- <a name="__input_prompt"></a><a href="#user-content-__input_prompt"><code>prompt</code></a>: _(Optional, default: `You are a helpful assistant.`)_ A string passed to the Qwen Code CLI's [`--prompt` argument](./configuration/settings.md#command-line-arguments-table).

- <a name="__input_settings"></a><a href="#user-content-__input_settings"><code>settings</code></a>: _(Optional)_ A JSON string written to `.qwen/settings.json` to configure the CLI's _project_ settings.
  For more details, see the documentation on [settings files](./configuration/settings.md#settings-files).

- <a name="__input_use_qwen_code_assist"></a><a href="#user-content-__input_use_qwen_code_assist"><code>use*qwen_code_assist</code></a>: *(Optional, default: `false`)\_ Whether to use Code Assist for Qwen Code model access instead of the default Qwen Code API key.
  For more information, see the [Qwen Code CLI documentation](./configuration/auth.md).

- <a name="__input_use_vertex_ai"></a><a href="#user-content-__input_use_vertex_ai"><code>use*vertex_ai</code></a>: *(Optional, default: `false`)\_ Whether to use Vertex AI for Qwen Code model access instead of the default Qwen Code API key.
  For more information, see the [Qwen Code CLI documentation](./configuration/auth.md).

- <a name="__input_extensions"></a><a href="#user-content-__input_extensions"><code>extensions</code></a>: _(Optional)_ A list of Qwen Code CLI extensions to install.

- <a name="__input_upload_artifacts"></a><a href="#user-content-__input_upload_artifacts"><code>upload*artifacts</code></a>: *(Optional, default: `false`)\_ Whether to upload artifacts to the github action.

- <a name="__input_use_pnpm"></a><a href="#user-content-__input_use_pnpm"><code>use*pnpm</code></a>: *(Optional, default: `false`)\_ Whether or not to use pnpm instead of npm to install qwen-code-cli

- <a name="__input_workflow_name"></a><a href="#user-content-__input_workflow_name"><code>workflow*name</code></a>: *(Optional, default: `${{ github.workflow }}`)\_ The GitHub workflow name, used for telemetry purposes.



### Outputs



- <a name="__output_summary"></a><a href="#user-content-__output_summary"><code>summary</code></a>: The summarized output from the Qwen Code CLI execution.

- <a name="__output_error"></a><a href="#user-content-__output_error"><code>error</code></a>: The error output from the Qwen Code CLI execution, if any.



### Repository Variables

We recommend setting the following values as repository variables so they can be reused across all workflows. Alternatively, you can set them inline as action inputs in individual workflows or to override repository-level values.

| Name               | Description                                               | Type     | Required | When Required             |
| ------------------ | --------------------------------------------------------- | -------- | -------- | ------------------------- |
| `DEBUG`            | Enables debug logging for the Qwen Code CLI.              | Variable | No       | Never                     |
| `QWEN_CLI_VERSION` | Controls which version of the Qwen Code CLI is installed. | Variable | No       | Pinning the CLI version   |
| `APP_ID`           | GitHub App ID for custom authentication.                  | Variable | No       | Using a custom GitHub App |

To add a repository variable:

1. Go to your repository's **Settings > Secrets and variables > Actions > New variable**.
2. Enter the variable name and value.
3. Save.

For details about repository variables, refer to the [GitHub documentation on variables][variables].

### Secrets

You can set the following secrets in your repository:

| Name              | Description                                   | Required | When Required                              |
| ----------------- | --------------------------------------------- | -------- | ------------------------------------------ |
| `QWEN_API_KEY`    | Your Qwen API key from DashScope.             | Yes      | Required for all workflows that call Qwen. |
| `APP_PRIVATE_KEY` | Private key for your GitHub App (PEM format). | No       | Using a custom GitHub App.                 |

To add a secret:

1. Go to your repository's **Settings > Secrets and variables >Actions > New repository secret**.
2. Enter the secret name and value.
3. Save.

For more information, refer to the [official GitHub documentation on creating and using encrypted secrets][secrets].

## Authentication

This action requires authentication to the GitHub API and optionally to Qwen Code services.

### GitHub Authentication

You can authenticate with GitHub in two ways:

1. **Default `GITHUB_TOKEN`:** For simpler use cases, the action can use the
   default `GITHUB_TOKEN` provided by the workflow.
2. **Custom GitHub App (Recommended):** For the most secure and flexible
   authentication, we recommend creating a custom GitHub App.

For detailed setup instructions for both Qwen and GitHub authentication, go to the
[**Authentication documentation**](./configuration/auth).

## Extensions

The Qwen Code CLI can be extended with additional functionality through extensions.
These extensions are installed from source from their GitHub repositories.

For detailed instructions on how to set up and configure extensions, go to the
[Extensions documentation](./extension/introduction.md).

## Best Practices

To ensure the security, reliability, and efficiency of your automated workflows, we strongly recommend following our best practices. These guidelines cover key areas such as repository security, workflow configuration, and monitoring.

Key recommendations include:

- **Securing Your Repository:** Implementing branch and tag protection, and restricting pull request approvers.
- **Monitoring and Auditing:** Regularly reviewing action logs and enabling OpenTelemetry for deeper insights into performance and behavior.

For a comprehensive guide on securing your repository and workflows, please refer to our [**Best Practices documentation**](./common-workflow).

## Customization

Create a QWEN.md file in the root of your repository to provide
project-specific context and instructions to [Qwen Code CLI](./common-workflow). This is useful for defining
coding conventions, architectural patterns, or other guidelines the model should
follow for a given repository.

## Contributing

Contributions are welcome! Check out the Qwen Code CLI **Contributing Guide** for more details on how to get started.

[secrets]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
[Qwen Code]: https://github.com/QwenLM/qwen-code
[DashScope]: https://dashscope.console.aliyun.com/apiKey
[Qwen Code CLI]: https://github.com/QwenLM/qwen-code-action/
[variables]: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-variables#creating-configuration-variables-for-a-repository
[GitHub CLI]: https://docs.github.com/en/github-cli/github-cli
[QWEN.md]: ./configuration/settings.md#context-files-hierarchical-instructional-context

---

### Users Integration Jetbrains (`docs/users/integration-jetbrains.md`)

# JetBrains IDEs

> JetBrains IDEs provide native support for AI coding assistants through the Agent Client Protocol (ACP). This integration allows you to use Qwen Code directly within your JetBrains IDE with real-time code suggestions.

### Features

- **Native agent experience**: Integrated AI assistant panel within your JetBrains IDE
- **Agent Client Protocol**: Full support for ACP enabling advanced IDE interactions
- **Symbol management**: #-mention files to add them to the conversation context
- **Conversation history**: Access to past conversations within the IDE
- **Reasoning effort**: Choose Default, Low, Medium, High, Extra high, or Max from the agent's session options; each provider maps or clamps the requested tier for the active model
- **Context usage**: See the current context-window occupancy while Qwen Code works

### Requirements

- JetBrains IDE with ACP support (IntelliJ IDEA, WebStorm, PyCharm, etc.)
- Qwen Code CLI installed

### Installation

#### Install from ACP Registry (Recommend)

1. Install Qwen Code CLI:

   ```bash
   npm install @qwen-code/qwen-code
   ```

2. Open your JetBrains IDE and navigate to AI Chat tool window.

3. Click **Add ACP Agent**, then click **Install**.

   

   For users using JetBrains AI Assistant and/or other ACP agents, click **Install From ACP Registry** in Agents List, then install Qwen Code ACP.

   

4. The Qwen Code agent should now be available in the AI Assistant panel.

   

#### Manual Install (for older version of JetBrains IDEs)

1. Install Qwen Code CLI:

   ```bash
   npm install @qwen-code/qwen-code
   ```

2. Open your JetBrains IDE and navigate to AI Chat tool window.

3. Click the 3-dot menu in the upper-right corner and select **Configure ACP Agent** and configure Qwen Code with the following settings:

```json
{
  "agent_servers": {
    "qwen": {
      "command": "/path/to/qwen",
      "args": ["--acp"],
      "env": {}
    }
  }
}
```

4. The Qwen Code agent should now be available in the AI Assistant panel



## Troubleshooting

### Agent not appearing

- Run `qwen --version` in terminal to verify installation
- Ensure your JetBrains IDE version supports ACP
- Restart your JetBrains IDE

### Qwen Code not responding

- Check your internet connection
- Verify CLI works by running `qwen` in terminal
- [File an issue on GitHub](https://github.com/qwenlm/qwen-code/issues) if the problem persists

---

### Users Integration Vscode (`docs/users/integration-vscode.md`)

# Visual Studio Code

> The VS Code extension (Beta) lets you see Qwen's changes in real-time through a native graphical interface integrated directly into your IDE, making it easier to access and interact with Qwen Code.

<br/>

<video src="https://cloud.video.taobao.com/vod/IKKwfM-kqNI3OJjM_U8uMCSMAoeEcJhs6VNCQmZxUfk.mp4" controls width="800">
  Your browser does not support the video tag.
</video>

### Features

- **Native IDE experience**: Dedicated Qwen Code sidebar panel accessed via the Qwen icon
- **Auto-accept edits mode**: Automatically apply Qwen's changes as they're made
- **File management**: @-mention files or attach files and images using the system file picker
- **Conversation history**: Access to past conversations
- **Multiple sessions**: Run multiple Qwen Code sessions simultaneously

### Requirements

- VS Code 1.96.0 or higher

### Installation

Download and install the extension from the [Visual Studio Code Extension Marketplace](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion).

## Troubleshooting

### Extension not installing

- Ensure you have VS Code 1.96.0 or higher
- Check that VS Code has permission to install extensions
- Try installing directly from the Marketplace website

### Qwen Code not responding

- Check your internet connection
- Start a new conversation to see if the issue persists
- [File an issue on GitHub](https://github.com/qwenlm/qwen-code/issues) if the problem continues

---

### Users Integration Zed (`docs/users/integration-zed.md`)

# Zed Editor

> Zed Editor provides native support for AI coding assistants through the Agent Client Protocol (ACP). This integration allows you to use Qwen Code directly within Zed's interface with real-time code suggestions.



### Features

- **Native agent experience**: Integrated AI assistant panel within Zed's interface
- **Agent Client Protocol**: Full support for ACP enabling advanced IDE interactions
- **File management**: @-mention files to add them to the conversation context
- **Conversation history**: Access to past conversations within Zed

### Requirements

- Zed Editor (latest version recommended)
- Qwen Code CLI installed

### Installation

#### Install from ACP Registry (Recommend)

1. Install Qwen Code CLI:

```bash
npm install @qwen-code/qwen-code
```

2. Download and install [Zed Editor](https://zed.dev/)

3. In Zed, click the **settings button** in the top right corner, select **"Add agent"**, choose **"Install from Registry"**, find **Qwen Code**, then click **Install**.

   

   

#### Manual Install

1. Install Qwen Code CLI:

```bash
npm install @qwen-code/qwen-code
```

2. Download and install [Zed Editor](https://zed.dev/)

3. In Zed, click the **settings button** in the top right corner, select **"Add agent"**, choose **"Create a custom agent"**, and add the following configuration:

```json
"Qwen Code": {
  "type": "custom",
  "command": "qwen",
  "args": ["--acp"],
  "env": {}
}
```



## Troubleshooting

### Agent not appearing

- Run `qwen --version` in terminal to verify installation
- Check that the JSON configuration is valid
- Restart Zed Editor

### Qwen Code not responding

- Check your internet connection
- Verify CLI works by running `qwen` in terminal
- [File an issue on GitHub](https://github.com/qwenlm/qwen-code/issues) if the problem persists

---

### Design Fork Subagent Fork Subagent Design (`docs/design/fork-subagent/fork-subagent-design.md`)

# Fork Subagent Design

> Implicit fork subagent that inherits the parent's full conversation context and shares prompt cache for cost-efficient parallel task execution.

## Overview

When the Agent tool is called without `subagent_type`, it triggers an implicit **fork** — a background subagent that inherits the parent's conversation history, system prompt, and tool definitions. The fork uses `CacheSafeParams` to ensure its API requests share the same prefix as the parent's, enabling DashScope prompt cache hits.

## Architecture

```
Parent conversation: [SystemPrompt | Tools | Msg1 | Msg2 | ... | MsgN (model)]
                              ↑ identical prefix for all forks ↑

Fork A: [...MsgN | placeholder results | "Research A"]  ← shared cache
Fork B: [...MsgN | placeholder results | "Modify B"]    ← shared cache
Fork C: [...MsgN | placeholder results | "Test C"]      ← shared cache
```

## Key Components

### 1. FORK_AGENT (`forkSubagent.ts`)

Synthetic agent config, not registered in `builtInAgents`. Has a fallback `systemPrompt` but in practice uses the parent's rendered system prompt via `generationConfigOverride`.

### 2. CacheSafeParams Integration (`agent.ts` + `forkedQuery.ts`)

```
agent.ts (fork path)
  │
  ├── getCacheSafeParams()          ← parent's generationConfig snapshot
  │     ├── generationConfig        ← systemInstruction + tools + temp/topP
  │     └── history                 ← (not used — we build extraHistory instead)
  │
  ├── forkGenerationConfig          ← passed as generationConfigOverride
  └── forkToolsOverride             ← FunctionDeclaration[] extracted from tools
        │
        ▼
  AgentHeadless.execute(context, signal, {
    extraHistory,                   ← parent conversation history
    generationConfigOverride,       ← parent's exact systemInstruction + tools
    toolsOverride,                  ← parent's exact tool declarations
  })
        │
        ▼
  AgentCore.createChat(context, {
    extraHistory,
    generationConfigOverride,       ← bypasses buildChatSystemPrompt()
  })                                   AND skips getInitialChatHistory()
        │                              (extraHistory already has env context)
        ▼
  new GeminiChat(config, generationConfig, startHistory)
                          ↑ byte-identical to parent's config
```

### 3. History Construction (`agent.ts` + `forkSubagent.ts`)

The fork's `extraHistory` must end with a model message to maintain Gemini API's user/model alternation when `agent-headless` sends the `task_prompt`.

Three cases:

| Parent history ends with      | extraHistory construction                                              | task_prompt                    |
| ----------------------------- | ---------------------------------------------------------------------- | ------------------------------ |
| `model` (no function calls)   | `[...rawHistory]` (unchanged)                                          | `buildChildMessage(directive)` |
| `model` (with function calls) | `[...rawHistory, model(clone), user(responses+directive), model(ack)]` | `'Begin.'`                     |
| `user` (unusual)              | `rawHistory.slice(0, -1)` (drop trailing user)                         | `buildChildMessage(directive)` |

### 4. Recursive Fork Prevention (`forkSubagent.ts`)

`isInForkChild()` scans conversation history for the `<fork-boilerplate>` tag. If found, the fork attempt is rejected with an error message.

### 5. Background Execution (`agent.ts`)

Fork uses `void executeSubagent()` (fire-and-forget) and returns `FORK_PLACEHOLDER_RESULT` immediately to the parent. Errors in the background task are caught, logged, and reflected in the display state.

## Data Flow

```
1. Model calls Agent tool (no subagent_type)
2. agent.ts: import forkSubagent.js
3. agent.ts: getCacheSafeParams() → forkGenerationConfig + forkToolsOverride
4. agent.ts: build extraHistory from parent's getHistory(true)
5. agent.ts: build forkTaskPrompt (directive or 'Begin.')
6. agent.ts: createAgentHeadless(FORK_AGENT, ...)
7. agent.ts: void executeSubagent() — background
8. agent.ts: return FORK_PLACEHOLDER_RESULT to parent immediately
9. Background:
   a. AgentHeadless.execute(context, signal, {extraHistory, generationConfigOverride, toolsOverride})
   b. AgentCore.createChat() — uses parent's generationConfig (cache-shared)
   c. runReasoningLoop() — uses parent's tool declarations
   d. Fork executes tools, produces result
   e. updateDisplay() with final status
```

## Graceful Degradation

If `getCacheSafeParams()` returns null (first turn, no history yet), the fork falls back to:

- `FORK_AGENT.systemPrompt` for system instruction
- `prepareTools()` for tool declarations

This ensures the fork always works, even without cache sharing.

## Files

| File                                                 | Role                                                                                  |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `packages/core/src/agents/runtime/forkSubagent.ts`   | FORK_AGENT config, buildForkedMessages(), isInForkChild(), buildChildMessage()        |
| `packages/core/src/tools/agent.ts`                   | Fork path: CacheSafeParams retrieval, extraHistory construction, background execution |
| `packages/core/src/agents/runtime/agent-headless.ts` | execute() options: generationConfigOverride, toolsOverride                            |
| `packages/core/src/agents/runtime/agent-core.ts`     | CreateChatOptions.generationConfigOverride                                            |
| `packages/core/src/followup/forkedQuery.ts`          | CacheSafeParams infrastructure (existing, no changes)                                 |

---

### Design Compact Mode Compact Mode Design (`docs/design/compact-mode/compact-mode-design.md`)

# Compact Mode Design: Competitive Analysis & Optimization

> Historical design. The current Web Shell behavior is documented in
> [Web Shell compact mode and tool progress](../web-shell-thinking-and-tool-progress.md).

> Ctrl+O compact/verbose mode toggle — competitive analysis with Claude Code, current implementation review, and optimization recommendations.
>
> User documentation: [Settings — ui.compactMode](../../users/configuration/settings.md).

## 1. Executive Summary

Qwen Code and Claude Code both provide a Ctrl+O shortcut for toggling between compact and detailed tool output views, but the **design philosophy, default state, and interaction model differ fundamentally**. This document provides a deep source-level comparison, identifies UX gaps, and proposes optimizations for Qwen Code.

| Dimension            | Claude Code                                 | Qwen Code                                     |
| -------------------- | ------------------------------------------- | --------------------------------------------- |
| Default mode         | Compact (verbose=false)                     | Verbose (compactMode=false)                   |
| Toggle semantics     | Temporary peek at details                   | Persistent preference switch                  |
| Persistence          | Session-only, resets on restart             | Persisted to settings.json                    |
| Scope                | Global screen switch (prompt ↔ transcript) | Per-component rendering toggle                |
| Frozen snapshot      | None (no concept)                           | None (removed)                                |
| Per-tool expand hint | Yes ("ctrl+o to expand")                    | Yes ("Press Ctrl+O to show full tool output") |

## 2. Claude Code Implementation Analysis

### 2.1 Architecture

Claude Code uses a **screen-based** approach rather than a component-level rendering toggle:

```
┌──────────────────────────────────┐
│         AppState (Zustand)       │
│  verbose: boolean (default: false)│
│  screen: 'prompt' | 'transcript' │
└──────────┬───────────────────────┘
           │
     ┌─────┴──────┐
     │  Ctrl+O    │  toggles screen mode
     │  Handler    │  NOT a rendering flag
     └─────┬──────┘
           │
     ┌─────▼──────────────┐
     │    REPL.tsx         │
     │  screen='prompt'  → compact view (default)
     │  screen='transcript'→ detailed view
     └────────────────────┘
```

### 2.2 Key Source Files

| Component        | File                                               | Key Logic                                               |
| ---------------- | -------------------------------------------------- | ------------------------------------------------------- |
| Toggle handler   | `src/hooks/useGlobalKeybindings.tsx:90-132`        | Switches `screen` between `'prompt'` and `'transcript'` |
| Keybinding       | `src/keybindings/defaultBindings.ts:44`            | `app:toggleTranscript`                                  |
| State definition | `src/state/AppStateStore.ts:472`                   | `verbose: false` (session-only)                         |
| Expand hint      | `src/components/CtrlOToExpand.tsx:29-46`           | Per-tool "(ctrl+o to expand)" text                      |
| Message filter   | `src/components/Messages.tsx:93-151`               | `filterForBriefTool()` for compact view                 |
| Permission       | `src/components/permissions/PermissionRequest.tsx` | Rendered in overlay layer, never hidden                 |

### 2.3 Design Decisions

1. **Compact is the default.** Users see a clean interface out of the box; detail is opt-in.
2. **Session-scoped.** `verbose` resets to `false` on every new session — Claude Code assumes users generally prefer the compact view and only need details temporarily.
3. **Screen-level toggle.** Ctrl+O doesn't change how components render; it switches the entire display between a "prompt" screen (compact) and a "transcript" screen (detailed).
4. **No frozen snapshot.** There is no snapshot freezing concept. When toggling, the display updates immediately with current state.
5. **Permission dialogs are separate.** Tool approvals are rendered in a dedicated overlay layer that is never affected by the verbose/compact toggle.
6. **Per-tool hint.** `CtrlOToExpand` component shows a contextual hint on individual tools when they produce large output, suppressed in sub-agents.

### 2.4 User Flow

```
Session start → compact mode (default)
     │
     ├─ Tool outputs are summarized in a single line
     ├─ Large tool output shows "(ctrl+o to expand)" hint
     │
     ├─ User presses Ctrl+O
     │     └─→ Screen switches to transcript (detailed view)
     │         └─ User sees all tool output, thinking, etc.
     │
     ├─ User presses Ctrl+O again
     │     └─→ Screen switches back to prompt (compact)
     │
     └─ Session ends → verbose resets to false
```

## 3. Qwen Code Implementation Analysis

### 3.1 Architecture

Qwen Code uses a **component-level rendering flag** that each UI component reads from context:

```
┌─────────────────────────────────────┐
│      CompactModeContext             │
│  compactMode: boolean (default: false)│
│  setCompactMode: (v) => void        │
└──────────┬──────────────────────────┘
           │
     ┌─────┴──────┐
     │  Ctrl+O    │  toggles compactMode
     │  Handler    │  persists to settings
     └─────┬──────┘
           │
     ┌─────▼──────────────────┐
     │  Each component reads  │
     │  compactMode and       │
     │  decides how to render │
     └────────────────────────┘
           │
     ┌─────▼──────────────────────────────┐
     │  ToolGroupMessage                   │
     │    showCompact = compactMode        │
     │      && !hasConfirmingTool          │
     │      && !hasErrorTool               │
     │      && !isEmbeddedShellFocused     │
     │      && !isUserInitiated            │
     └────────────────────────────────────┘
```

### 3.2 Key Source Files

| Component       | File                                  | Key Logic                                       |
| --------------- | ------------------------------------- | ----------------------------------------------- |
| Toggle handler  | `AppContainer.tsx:1684-1690`          | Toggles `compactMode`, persists to settings     |
| Context         | `CompactModeContext.tsx`              | `compactMode`, `setCompactMode`                 |
| Tool group      | `ToolGroupMessage.tsx:105-110`        | `showCompact` with 4 force-expand conditions    |
| Tool message    | `ToolMessage.tsx:346-350`             | Hides `displayRenderer` in compact mode         |
| Compact display | `CompactToolGroupDisplay.tsx:49-108`  | Single-line summary with status + hint          |
| Confirmation    | `ToolConfirmationMessage.tsx:113-147` | Simplified 3-option compact approval            |
| Tips            | `Tips.tsx:14-29`                      | Startup tip rotation includes compact mode hint |
| Settings sync   | `SettingsDialog.tsx:189-193`          | Syncs with CompactModeContext + refreshStatic   |
| MainContent     | `MainContent.tsx:60-76`               | Renders live pendingHistoryItems                |
| Thinking        | `HistoryItemDisplay.tsx:123-133`      | Hides `gemini_thought` in compact mode          |

### 3.3 Design Decisions

1. **Verbose is the default.** Users see all tool output and thinking by default.
2. **Persistent preference.** `compactMode` is saved to `settings.json` and survives across sessions.
3. **Component-level rendering.** Each component reads `compactMode` from context and adjusts its own rendering.
4. **Force-expand protection.** Four conditions override compact mode to ensure critical UI elements are always visible (confirmations, errors, shell, user-initiated).
5. **No snapshot freezing.** The toggle always shows live output — no frozen snapshots.
6. **Settings dialog sync.** Toggling compact mode from Settings updates React state immediately via `setCompactMode`.
7. **Non-intrusive discoverability.** Compact mode is introduced via the startup Tips rotation rather than a persistent footer indicator, avoiding UI clutter.

### 3.4 User Flow

```
Session start → verbose mode (default)
     │
     ├─ All tool outputs, thinking, details visible
     │
     ├─ User presses Ctrl+O (or toggles in Settings)
     │     └─→ compactMode = true, persisted
     │         ├─ Tool groups show single-line summary
     │         ├─ Thinking/thought content hidden
     │         └─ Confirmations, errors, shell still expanded
     │
     ├─ User presses Ctrl+O again
     │     └─→ compactMode = false, persisted
     │         └─ All details visible again
     │
     └─ Next session → same mode as last session
```

## 4. Key Differences Deep Dive

### 4.1 Default Mode Philosophy

| Aspect               | Claude Code (compact default)         | Qwen Code (verbose default)                   |
| -------------------- | ------------------------------------- | --------------------------------------------- |
| First impression     | Clean, minimal — professional feel    | Information-rich — full transparency          |
| Learning curve       | User must learn Ctrl+O to see details | User can immediately see everything           |
| Target audience      | Experienced users who trust the tool  | Users who want to understand what's happening |
| Information overload | Avoided by default                    | Possible for new users                        |
| Discoverability      | Per-tool "(ctrl+o to expand)" hints   | Startup Tips rotation + ? shortcuts + /help   |

**Analysis:** Claude Code's compact default works because its user base is generally experienced developers who trust the tool and don't need to see every tool invocation. Qwen Code's verbose default is appropriate for its earlier stage where building user trust through transparency is important.

### 4.2 Persistence Model

| Aspect           | Claude Code               | Qwen Code                  |
| ---------------- | ------------------------- | -------------------------- |
| Persisted?       | No — session-only         | Yes — to settings.json     |
| Rationale        | Verbose is temporary peek | Mode is user preference    |
| Restart behavior | Always starts compact     | Starts with last-used mode |

**Analysis:** Claude Code treats detail viewing as a momentary need — you look, then go back. Qwen Code treats it as a stable preference — some users always want details, others always want compact. Both are valid; Qwen Code's approach is more flexible.

### 4.3 Confirmation Protection

| Aspect                  | Claude Code                                 | Qwen Code                                            |
| ----------------------- | ------------------------------------------- | ---------------------------------------------------- |
| Mechanism               | Overlay/modal layer (structurally separate) | Force-expand conditions in `showCompact`             |
| Coverage                | Complete — approvals can never be hidden    | Complete — 4 conditions cover all interactive states |
| Compact confirmation UI | N/A (overlay is always full)                | Simplified 3-option RadioButtonSelect                |

**Analysis:** Claude Code's architectural separation (overlay layer) is more robust. Qwen Code's force-expand approach is effective but requires each new interactive state to be explicitly added to the condition list.

### 4.4 Rendering Approach

| Aspect       | Claude Code                         | Qwen Code                                  |
| ------------ | ----------------------------------- | ------------------------------------------ |
| Toggle scope | Screen-level (prompt ↔ transcript) | Component-level (each component decides)   |
| Granularity  | All-or-nothing                      | Fine-grained per component                 |
| Flexibility  | Low — global switch                 | High — components can override             |
| Consistency  | Guaranteed                          | Depends on each component's implementation |

**Analysis:** Qwen Code's component-level approach is more flexible (e.g., force-expand for specific conditions) but requires more discipline to maintain consistency. Claude Code's screen-level approach is simpler and guarantees consistent behavior.

## 5. Optimization Recommendations

### 5.1 [P0] Keep Verbose as Default — No Change Needed

Qwen Code's verbose default is the right choice for its current stage. Users who are new to the tool need transparency to build trust. As the product matures, consider making compact the default (like Claude Code).

### 5.2 [P1] Per-Tool Expansion for Large Outputs

Claude Code shows "(ctrl+o to expand)" on individual tools that produce large output. Qwen Code currently only has a global toggle. Consider:

- When a single tool produces output exceeding N lines, show a per-tool "expand" hint in compact mode.
- Scope: future enhancement, not current priority.

### 5.3 [P2] Consider Session-Scoped Override

Some users may want compact mode as their default but occasionally need verbose for a specific session. Consider supporting both:

- `settings.json` → persistent default (current behavior)
- Ctrl+O during session → temporary override for current session only (Claude Code behavior)
- On session restart → revert to settings.json value

This gives users the best of both worlds. Implementation would require separating "settings default" from "session override" state.

### 5.4 [P2] Structural Separation for Confirmations

Currently, confirmation protection relies on `showCompact` conditions in `ToolGroupMessage`. Consider a more robust approach:

- Render confirmations in a separate layer (like Claude Code's overlay approach).
- This would make it architecturally impossible for compact mode to affect confirmations.
- Lower priority since the current force-expand approach works correctly.

## 6. Current Implementation Status

After the `feat/compact-mode-optimization` branch changes:

| Feature                          | Status | Notes                                             |
| -------------------------------- | ------ | ------------------------------------------------- |
| Startup Tips hint                | Done   | Compact mode tip in Tips rotation (non-intrusive) |
| Ctrl+O in keyboard shortcuts (?) | Done   | Added to KeyboardShortcuts component              |
| Ctrl+O in /help                  | Done   | Added to Help component                           |
| Settings dialog sync             | Done   | Syncs compactMode with CompactModeContext         |
| No snapshot freezing             | Done   | Toggle always shows live output                   |
| Confirmation protection          | Done   | Force-expand + WaitingForConfirmation guard       |
| Shell protection                 | Done   | `!isEmbeddedShellFocused` force-expand            |
| Error protection                 | Done   | `!hasErrorTool` force-expand                      |
| User docs updated                | Done   | settings.md, keyboard-shortcuts.md                |

## 7. File Reference

### Qwen Code

| File                                                                  | Purpose                                                |
| --------------------------------------------------------------------- | ------------------------------------------------------ |
| `packages/cli/src/ui/AppContainer.tsx`                                | Toggle handler, state initialization, context provider |
| `packages/cli/src/ui/contexts/CompactModeContext.tsx`                 | Context definition                                     |
| `packages/cli/src/ui/components/messages/ToolGroupMessage.tsx`        | Force-expand logic                                     |
| `packages/cli/src/ui/components/messages/ToolMessage.tsx`             | Per-tool output hiding                                 |
| `packages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsx` | Compact view rendering                                 |
| `packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx` | Compact confirmation UI                                |
| `packages/cli/src/ui/components/MainContent.tsx`                      | Pending history items rendering                        |
| `packages/cli/src/ui/components/Tips.tsx`                             | Startup tip with compact mode hint                     |
| `packages/cli/src/ui/components/Help.tsx`                             | /help shortcut entry                                   |
| `packages/cli/src/ui/components/KeyboardShortcuts.tsx`                | ? shortcut entry                                       |
| `packages/cli/src/ui/components/SettingsDialog.tsx`                   | Settings sync                                          |
| `packages/cli/src/ui/components/HistoryItemDisplay.tsx`               | Thinking content hiding                                |
| `packages/cli/src/config/settingsSchema.ts`                           | Setting definition                                     |
| `packages/cli/src/config/keyBindings.ts`                              | Ctrl+O binding                                         |

### Claude Code (Reference)

| File                                               | Purpose                           |
| -------------------------------------------------- | --------------------------------- |
| `src/hooks/useGlobalKeybindings.tsx`               | Toggle handler                    |
| `src/state/AppStateStore.ts`                       | State definition (verbose: false) |
| `src/components/CtrlOToExpand.tsx`                 | Per-tool expand hint              |
| `src/components/Messages.tsx`                      | Brief message filter              |
| `src/screens/REPL.tsx`                             | Screen-level mode switching       |
| `src/components/permissions/PermissionRequest.tsx` | Overlay-based confirmation        |

---

### Design Channels Channels Design (`docs/design/channels/channels-design.md`)

# Channels Design

> External messaging integrations for Qwen Code — interact with an agent from Telegram, WeChat, and more.
>
> User documentation: [Channels Overview](../../users/features/channels/overview.md).

## Overview

A **channel** connects an external messaging platform to a Qwen Code agent. Configured in `settings.json`, managed via `qwen channel` subcommands, multi-user (each user gets an isolated ACP session).

## Architecture

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Platform Adapter** — connects to external API, translates messages to/from Envelopes. **ACP Bridge** — spawns `qwen-code --acp`, manages sessions, emits `textChunk`/`toolCall`/`disconnected` events. **Session Router** — maps senders to ACP sessions via namespaced keys (`<channel>:<sender>`). **Sender Gate** / **Group Gate** — access control (allowlist / pairing / open) and mention gating. **Channel Base** — abstract base with Template Method pattern: plugins override `connect`, `sendMessage`, `disconnect`. **Channel Registry** — `Map<string, ChannelPlugin>` with collision detection.

### Envelope

Normalized message format all platforms convert to:

- **Identity**: `senderId`, `senderName`, `chatId`, `channelName`
- **Content**: `text`, optional `imageBase64`/`imageMimeType`, optional `referencedText`
- **Context**: `isGroup`, `isMentioned`, `isReplyToBot`, optional `threadId`

Plugin responsibilities: `senderId` must be stable/unique; `chatId` must distinguish DMs from groups; boolean flags must be accurate for gate logic; @mentions stripped from `text`.

### Message Flow

```
Inbound:  User message → Adapter → GroupGate → SenderGate → Slash commands → SessionRouter → AcpBridge → Agent
Outbound: Agent response → AcpBridge → SessionRouter → Adapter → User
```

Slash commands (`/clear`, `/help`, `/status`) are handled in ChannelBase before reaching the agent.

### Sessions

One `qwen-code --acp` process with multiple ACP sessions. Scope per channel: **`user`** (default), **`thread`**, or **`single`**. Routing keys namespaced as `<channelName>:<key>`.

### Error Handling

- **Connection failures** — logged; service continues if at least one channel connects
- **Bridge crashes** — exponential backoff (max 3 retries), `setBridge()` on all channels, session restore
- **Session serialization** — per-session promise chains prevent concurrent prompt collisions

## Plugin System

The architecture is extensible — new adapters (including third-party) can be added without modifying core. Built-in channels use the same plugin interface (dogfooding).

### Plugin Contract

A `ChannelPlugin` declares `channelType`, `displayName`, `requiredConfigFields`, and a `createChannel()` factory. Plugins implement three methods:

| Method                      | Responsibility                                    |
| --------------------------- | ------------------------------------------------- |
| `connect()`                 | Connect to platform and register message handlers |
| `sendMessage(chatId, text)` | Format and deliver agent response                 |
| `disconnect()`              | Clean up on shutdown                              |

On inbound messages, plugins build an `Envelope` and call `this.handleInbound(envelope)` — the base class handles the rest: access control, group gating, pairing, session routing, prompt serialization, slash commands, instructions injection, reply context, and crash recovery.

### Extension Points

- Custom slash commands via `registerCommand()`
- Working indicators by wrapping `handleInbound()` with typing/reaction display
- Tool call hooks via `onToolCall()`
- Media handling by attaching to Envelope before `handleInbound()`

### Discovery & Loading

External plugins are **extensions** managed by `ExtensionManager`, declared in `qwen-extension.json`:

```json
{
  "name": "my-channel-extension",
  "version": "1.0.0",
  "channels": {
    "my-platform": {
      "entry": "dist/index.js",
      "displayName": "My Platform Channel"
    }
  }
}
```

Loading sequence at `qwen channel start`: load settings → register built-ins → scan extensions → dynamic import + validate → register (reject collisions) → validate config → `createChannel()` → `connect()`.

Plugins run in-process (no sandbox), same trust model as npm dependencies.

## Configuration

```jsonc
{
  "channels": {
    "my-telegram": {
      "type": "telegram",
      "token": "$TELEGRAM_BOT_TOKEN", // env var reference
      "senderPolicy": "allowlist", // allowlist | pairing | open
      "allowedUsers": ["123456"],
      "sessionScope": "user", // user | thread | single
      "cwd": "/path/to/project",
      "model": "qwen3.5-plus",
      "instructions": "Keep responses short.",
      "groupPolicy": "disabled", // disabled | allowlist | open
      "dmPolicy": "open", // open | disabled
      "groups": { "*": { "requireMention": true } },
    },
  },
}
```

Auth is plugin-specific: static token (Telegram), app credentials (DingTalk), QR code login (WeChat), proxy token (TMCP).

## CLI Commands

```bash
# Channels
qwen channel start [name]                     # start all or one channel
qwen channel stop                             # stop running service
qwen channel status                           # show channels, sessions, uptime
qwen channel pairing list <ch>                # pending pairing requests
qwen channel pairing approve <ch> <code>      # approve a request

# Extensions
qwen extensions install <path-or-package>     # install
qwen extensions link <local-path>             # symlink for dev
qwen extensions list                          # show installed
qwen extensions remove <name>                 # uninstall
```

## Package Structure

```
packages/channels/
├── base/                    # @qwen-code/channel-base
│   └── src/
│       ├── AcpBridge.ts     # ACP process lifecycle, session management
│       ├── SessionRouter.ts # sender ↔ session mapping, persistence
│       ├── SenderGate.ts    # allowlist / pairing / open
│       ├── GroupGate.ts     # group chat policy + mention gating
│       ├── PairingStore.ts  # pairing code generation + approval
│       ├── ChannelBase.ts   # abstract base: routing, slash commands
│       └── types.ts         # Envelope, ChannelConfig, etc.
├── telegram/                # @qwen-code/channel-telegram
├── weixin/                  # @qwen-code/channel-weixin
└── dingtalk/                # @qwen-code/channel-dingtalk
```

## Future Work

### Safety & Group Chat

- **Per-group tool restrictions** — `tools`/`toolsBySender` deny/allow lists per group
- **Group context history** — ring buffer of recent skipped messages, prepended on @mention
- **Regex mention patterns** — fallback `mentionPatterns` for unreliable @mention metadata
- **Per-group instructions** — `instructions` field on `GroupConfig` for per-group personas
- **`/activation` command** — runtime toggle for `requireMention`, persisted to disk

### Operational Tooling

- **`qwen channel doctor`** — config validation, env vars, bot tokens, network checks
- **`qwen channel status --probe`** — real connectivity checks per channel

### Platform Expansion

- **Discord** — Bot API + Gateway, servers/channels/DMs/threads
- **Slack** — Bolt SDK, Socket Mode, workspaces/channels/DMs/threads

### Multi-Agent

- **Multi-agent routing** — multiple agents with bindings per channel/group/user
- **Broadcast groups** — multiple agents respond to the same message

### Plugin Ecosystem

- **Community plugin template** — `create-qwen-channel` scaffolding tool
- **Plugin registry/discovery** — `qwen extensions search`, version compatibility

---

### Design Auto Memory Memory System (`docs/design/auto-memory/memory-system.md`)

# Memory 记忆管理系统

> 本文介绍 Qwen Code 中 **Managed Auto-Memory**（托管自动记忆）的记忆管理机制、触发时机和实现细节。

---

## 目录

1. [概述](#概述)
2. [存储结构](#存储结构)
3. [记忆类型](#记忆类型)
4. [记忆条目格式](#记忆条目格式)
5. [核心生命周期](#核心生命周期)
6. [Extract — 提取](#extract--提取)
7. [Dream — 整合](#dream--整合)
8. [Recall — 召回](#recall--召回)
9. [Forget — 遗忘](#forget--遗忘)
10. [索引重建](#索引重建)
11. [遥测埋点](#遥测埋点)

---

## 概述

Managed Auto-Memory 是一套在 AI 会话过程中**自动**积累、整合和检索用户相关知识的持久化记忆系统。它通过四个核心操作维护记忆的生命周期：

| 操作 | 英文    | 触发方式                   | 作用                                   |
| ---- | ------- | -------------------------- | -------------------------------------- |
| 提取 | Extract | 自动（每轮对话后）         | 从对话记录中提炼新知识写入记忆文件     |
| 整合 | Dream   | 自动（周期性后台任务）     | 对记忆文件去重、合并，保持整洁         |
| 召回 | Recall  | 自动（每轮对话前）         | 检索与当前请求相关的记忆注入到系统提示 |
| 遗忘 | Forget  | 手动（用户命令 `/forget`） | 精确删除指定的记忆条目                 |

---

## 存储结构

### 目录布局

```
~/.qwen/                                      ← 全局基础目录（默认）
└── projects/
    └── <sanitized-git-root>/                 ← 项目标识（基于 Git 根路径）
        ├── meta.json                         ← 元数据（提取/整合时间戳、状态）
        ├── extract-cursor.json               ← 提取游标（已处理的对话偏移量）
        ├── consolidation.lock                ← Dream 进程互斥锁
        └── memory/                           ← 记忆主目录
            ├── MEMORY.md                     ← 索引文件（自动生成，汇总所有条目）
            ├── user.md                       ← 用户偏好记忆（示例）
            ├── feedback.md                   ← 反馈规范记忆（示例）
            ├── project/
            │   └── milestone.md              ← 项目记忆（支持子目录）
            └── reference/
                └── grafana.md                ← 外部资源记忆
```

> **环境变量覆盖**：
>
> - `QWEN_CODE_MEMORY_BASE_DIR`：替换全局基础目录
> - `QWEN_CODE_MEMORY_LOCAL=1`：改用项目内路径 `.qwen/memory/`
> - `QWEN_CODE_MEMORY_PROJECT_SCOPE=workspace`：按精确 workspace 目录分区项目记忆。`qwen serve` 未显式设置或值为空白时会注入 `workspace`；standalone CLI 仍默认按 Git 根目录共享。非空取值会做 trim / 小写归一，无法识别的值会告警一次并回退到 `git-root`。
>   - 团队记忆（`getTeamAutoMemoryRoot`）仍按 Git 根目录分区：同一 checkout 内的嵌套 workspace 仍共享团队记忆——团队记忆本就应跨 workspace 共享，不随本开关改变。
>   - 切换 scope 不做迁移：切到 `workspace` 后，此前写在 git-root key 下的项目记忆会“失联”（切回去则看不到 workspace key 下新写的内容）。
>   - 目录 key 由 `sanitizeCwd` 生成（非字母数字字符替换为 `-`），仅在标点上不同的兄弟目录（如 `feature_1` 与 `feature-1`）会映射到同一记忆目录；`workspace` 分区下这类命名会共享记忆，命名时需避开。
>   - 待处理任务上限（`MAX_PENDING = 16`）是**每个 lane** 的，N 个 workspace 即允许 16·N 个并发记忆任务，目前没有守护进程级总量上限。

### 关键文件说明

| 文件                  | 说明                                                                   |
| --------------------- | ---------------------------------------------------------------------- |
| `meta.json`           | 记录最后一次 Extract / Dream 的时间、会话 ID、涉及的记忆类型、执行状态 |
| `extract-cursor.json` | 记录当前会话已处理到对话历史的哪个偏移量，避免重复提取                 |
| `consolidation.lock`  | Dream 运行时的文件锁，内容为持有者 PID，超过 1 小时自动失效            |
| `MEMORY.md`           | 所有主题文件的索引，每次 Extract/Dream 后重建，格式为 Markdown 列表    |

---

## 记忆类型

系统支持四种内置记忆类型，每种对应不同的信息维度：

| 类型        | 存储内容                                              | 何时写入                                 | 何时读取                     |
| ----------- | ----------------------------------------------------- | ---------------------------------------- | ---------------------------- |
| `user`      | 用户的角色、技能背景、工作习惯                        | 了解到用户角色/偏好/知识背景时           | 回答需要根据用户背景定制时   |
| `feedback`  | 用户对 AI 行为的指导：避免什么、继续什么              | 用户纠正 AI 或确认某种非显而易见的做法时 | 影响 AI 行为方式时           |
| `project`   | 项目进展、目标、决策、截止日期、Bug 追踪              | 了解到谁在做什么、为什么、截止何时时     | 帮助 AI 理解工作背景和动机时 |
| `reference` | 外部系统资源指针（Dashboard、工单系统、Slack 频道等） | 得知某种外部资源及其用途时               | 用户提及外部系统或相关信息时 |

**不应该存入记忆的内容**：代码模式/约定、Git 历史、调试方案、临时任务状态、已在 QWEN.md/AGENTS.md 中记录的内容。

---

## 记忆条目格式

每个主题文件使用 **YAML frontmatter + Markdown body** 格式：

```markdown
---
name: 记忆名称
description: 一句话描述（用于判断召回相关性，要具体）
type: user|feedback|project|reference
---

记忆主体内容（summary 行）

Why: 背后原因（让 AI 能理解边界情况而不是盲目遵守规则）
How to apply: 适用场景和使用方式
```

对于 `feedback` 和 `project` 类型，强烈建议填写 `Why` 和 `How to apply`，使记忆在边界情况下仍能正确应用。

---

## 核心生命周期

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Extract — 提取

### 触发时机

每次 AI 完成一轮响应后，由 `scheduleAutoMemoryExtract` 自动触发（后台非阻塞）。

### 调度逻辑（`extractScheduler.ts`）

```mermaid
flowchart TD
    A[scheduleAutoMemoryExtract 被调用] --> B{本轮历史记录中\n是否有写记忆文件的工具调用?}
    B -- 是 --> C[登记 skipped 任务\n原因: memory_tool]
    B -- 否 --> D{isExtractRunning?}
    D -- 是 --> E{是否已有 queued 请求?}
    E -- 是 --> F[更新 queued 请求的\nhistory 参数]
    E -- 否 --> G[注册 pending 任务\n放入 queue]
    D -- 否 --> H[注册 running 任务\n调用 runTask]
    H --> I[markExtractRunning\nsetCurrentTaskId]
    I --> J[runAutoMemoryExtract]
    J --> K[任务完成]
    K --> L[clearExtractRunning\n检查 queue → startQueuedIfNeeded]
    F --> M[返回 skipped: queued]
    G --> M
    C --> N[返回 skipped: memory_tool]
```

**跳过原因说明**：

| 原因              | 含义                                            |
| ----------------- | ----------------------------------------------- |
| `memory_tool`     | 本轮主 Agent 已直接写了记忆文件，跳过以避免冲突 |
| `already_running` | 提取正在进行且无法入队                          |
| `queued`          | 已有提取在运行，本次请求已入队                  |

### 核心提取流程（`extract.ts`）

```mermaid
flowchart TD
    A[runAutoMemoryExtract] --> B[ensureAutoMemoryScaffold\n初始化目录和文件]
    B --> C[readExtractCursor\n读取上次处理到的位置]
    C --> D[history.slice startOffset\n只取未处理的消息切片]
    D --> E{slice 有新的 user 消息?}
    E -- 否 --> F[更新 cursor\n返回无 patches 结果]
    E -- 是 --> G[runAutoMemoryExtractionByAgent\n调用 forked agent 提取]
    G --> H{有 touched topics?}
    H -- 是 --> I[bumpMetadata\n更新 meta.json]
    I --> J[rebuildManagedAutoMemoryIndex\n重建 MEMORY.md]
    J --> K[writeExtractCursor\n记录最新 offset = history.length]
    H -- 否 --> K
    K --> L[返回 AutoMemoryExtractResult]
```

> **注意：** `isUnderMemoryPressure` 门控位于 `MemoryManager.runExtract()` 中，不在本流程内。当 monitor 报告 hard/critical 压力时，`MemoryManager` 会跳过 extract 调用，不推进 cursor。

**提取游标（Cursor）**：

- 字段：`{ sessionId, processedOffset, updatedAt }`
- 提取前先通过 `readExtractCursor` 读取当前进度，再用 `history.slice(processedOffset)` 仅处理未读部分
- 每次提取后更新 `processedOffset` 为当前历史长度（`params.history.length`）
- 跨会话时（`sessionId` 变化）从偏移量 0 重新开始
- 注意：不再通过 `buildTranscriptMessages` / `loadUnprocessedTranscriptSlice` 构建转录文本——`hasNewUserMessages` 通过 `history.slice(startOffset).some(m => m.role === 'user' && partToString(m.parts).trim().length > 0)` 判断，仅在未读切片上做轻量字符串化，全量历史不再处理

**Patch 过滤规则**：

- 摘要长度 < 12 字符 → 丢弃
- 摘要以 `?` 结尾 → 丢弃（疑问句）
- 包含临时性关键词（today/now/currently/temporary 等）→ 丢弃
- 相同 `topic:summary` 组合 → 去重

---

## Dream — 整合

### 触发时机

每次 AI 完成一轮响应后，由 `scheduleManagedAutoMemoryDream` 自动触发（后台非阻塞）。但受多个门控条件保护，大多数情况下会被跳过。

### 调度门控（`dreamScheduler.ts`）

```mermaid
flowchart TD
    A[scheduleManagedAutoMemoryDream 被调用] --> B{Dream 功能是否启用?}
    B -- 否 --> C[跳过: disabled]
    B -- 是 --> D[ensureAutoMemoryScaffold\n读取 lastDreamSessionId]
    D --> E{当前 sessionId\n== lastDreamSessionId?}
    E -- 是 --> F[跳过: same_session]
    E -- 否 --> G{elapsedHours ≥ 24h\n或从未 dream?}
    G -- 否 --> H[跳过: min_hours]
    G -- 是 --> I{距上次 session scan\n< 10 分钟?}
    I -- 是 --> J[跳过: min_sessions\n等待下次扫描窗口]
    I -- 否 --> K[扫描 chats/*.jsonl mtime\n统计上次 Dream 后的新会话数]
    K --> L{新会话数 ≥ 5?}
    L -- 否 --> M[跳过: min_sessions]
    L -- 是 --> N{lockExists?\nPID 检查 + 过期检查}
    N -- 是 --> O[跳过: locked]
    N -- 否 --> P{dedupeKey 是否已有\n同项目 Dream 任务?}
    P -- 是 --> Q[跳过: running\n返回已有 taskId]
    P -- 否 --> R[调度后台任务\nBgTaskScheduler]
    R --> S[acquireDreamLock\n写入 PID 到 consolidation.lock]
    S --> T[runManagedAutoMemoryDream]
    T --> U[更新 meta.json\n释放锁]
```

**门控参数**：

| 参数                       | 默认值   | 说明                          |
| -------------------------- | -------- | ----------------------------- |
| `minHoursBetweenDreams`    | 24 小时  | 两次 Dream 之间的最小时间间隔 |
| `minSessionsBetweenDreams` | 5 个会话 | 触发 Dream 所需的最小新会话数 |
| `SESSION_SCAN_INTERVAL_MS` | 10 分钟  | 会话文件扫描的节流间隔        |
| `DREAM_LOCK_STALE_MS`      | 1 小时   | lock 文件被视为过期的时间阈值 |

**锁机制**：

- lock 文件位于 `<project-state-dir>/consolidation.lock`
- 内容为持有进程的 PID
- 检查时：若 PID 进程已不存在（`kill(pid, 0)` 失败）或 lock 超过 1 小时 → 视为过期，自动清除

### 整合执行流程（`dream.ts`）

```mermaid
flowchart TD
    A[runManagedAutoMemoryDream] --> B{是否配置了 Config?}
    B -- 是 --> C[Agent 路径\nplanManagedAutoMemoryDreamByAgent]
    C --> D{Agent 是否修改了文件?}
    D -- 是 --> E[从文件路径推断 touched topics]
    E --> F[bumpMetadata\n重建 MEMORY.md 索引]
    F --> G[updateDreamMetadataResult]
    G --> H[记录遥测事件]
    H --> I[返回结果]
    B -- 否 --> J[机械去重路径]
    C -- 抛出异常 --> J
    D -- 否 --> J

    J --> K[scanAutoMemoryTopicDocuments\n读取所有主题文件]
    K --> L[对每个文件执行 buildDreamedBody]
    L --> M[解析 entries → 按 summary 去重\n按字母升序排序 → 重新渲染]
    M --> N{body 有变化?}
    N -- 是 --> O[写回文件]
    O --> P[记录 touched topic]
    N --> Q[检查跨文件重复\ndedupeKey = type:summary]
    Q --> R{发现重复文件?}
    R -- 是 --> S[合并 entries 到 canonical 文件\n删除重复文件]
    S --> P
    R -- 否 --> T{有 touched topics?}
    P --> T
    T -- 是 --> U[bumpMetadata\n重建 MEMORY.md 索引]
    U --> V[updateDreamMetadataResult\n记录遥测 → 返回结果]
    T -- 否 --> V
```

**机械去重逻辑**：

1. 对每个主题文件内部：按 `summary.toLowerCase()` 去重，合并 `why`/`howToApply` 字段
2. 按 summary 字母顺序重新排序
3. 跨文件：相同 `type:summary` 的条目合并到最先发现的文件，删除重复文件

---

## Recall — 召回

### 触发时机

每轮 AI 处理用户请求之前，由 `resolveRelevantAutoMemoryPromptForQuery` 自动触发，将相关记忆注入系统提示词。

### 召回流程（`recall.ts`）

```mermaid
flowchart TD
    A[resolveRelevantAutoMemoryPromptForQuery] --> B[scanAllAutoMemoryTopicDocuments +\nscanAllUserAutoMemoryTopicDocuments\n扫描项目级与用户级全部主题文件]
    B --> C[filterExcludedAutoMemoryDocuments\n合并作用域并过滤排除列表中的文件]
    C --> D{query 为空\n或 docs 为空\n或 limit <= 0?}
    D -- 是 --> E[返回空 prompt\nstrategy: none]
    D -- 否 --> F{是否配置了 Config?}
    F -- 是 --> G[selectModelCandidateDocuments\n词法候选 + recent reserve\n最多 200 篇且交错排列]
    G --> H[selectRelevantAutoMemoryDocumentsByModel\n构建最多 25 KB manifest\n发起 side query 请求模型选择]
    H --> I{模型返回结果?}
    I -- 有文档 --> J[strategy: model]
    I -- 无文档 --> K[strategy: none\n仍然返回空]
    H -- "失败/异常" --> L[复用已计算的启发式排序]
    F -- 否 --> M[tokenize query\nNFKC + 非 CJK 字母整串 + CJK bigram\n最多 64 个 token]
    M --> N[scoreDocument 打分\ntitle +4 / description +3 / body +1\n词法命中后类型加成最多 +2]
    N --> O[过滤 score=0 的文档\n按分数降序、mtime 降序、输入顺序排列\n取 Top 5]
    L --> O
    O --> P{有得分文档?}
    P -- 是 --> Q[strategy: heuristic]
    P -- 否 --> K
    J --> R[buildRelevantAutoMemoryPrompt\n构建 Relevant Memory 区块]
    Q --> R
    R --> S[返回注入主系统提示的 prompt 片段]
```

> **关于 200 上限：这是换了截断依据，不是抬高了天花板。** 旧路径按 scope 各自
> 保留最近 200 篇，截断发生在看 query 之前；新路径先扫全量，再按词法相关性 +
> recency reserve 选出最多 200 篇候选。因此效果分三档：总量 ≤200 时两者都不按
> 数量丢弃（但新增了 25 KB manifest 上限，长 description 场景可能被截）；总量
> 在 200–400 且单个 scope 不超 200 时，旧路径会把全部（最多 400 篇）送给
> Selector，新路径最多 200 篇，且实测中 25 KB 的 manifest 预算会先于数量上限
> 生效——150+150 篇的实测里旧路径送 300 行、新路径只送 94 行，**候选变少的幅度
> 比数量上限暗示的更大**；只有单个 scope 超过 200 时，
> 才是这次真正要解决的场景——旧的 recency 上限会让老而相关的文档永久不可见。
> 详见 `docs/design/2026-08-09-bounded-memory-recall-candidates.md`。

**评分规则（启发式）**：

| 条件                                       | 加分                |
| ------------------------------------------ | ------------------- |
| query token 出现在 title                   | +4（每个 token）    |
| query token 出现在 description             | +3（每个 token）    |
| query token 出现在 body 前 1200 字符       | +1（每个 token）    |
| 至少一次词法命中后，token 是类型特征关键词 | +1，整篇文档最多 +2 |

> **Tokenize 规则**：NFKC 归一化并转小写后，Han/Hiragana/Katakana/Hangul 连续片段
> 按 code point bigram 切分（单字不产生 token）；其余至少 3 个字母、组合符或数字的
> 连续片段整串保留。后者基于 `\p{L}` 而非 `[a-z0-9]`，因此西里尔、希腊、阿拉伯和
> 带重音拉丁文都能产生 token。CJK 是**逐字符**排除的，不能只依赖正则分支顺序——
> `\p{L}` 也匹配 Han，否则 `abc漢字` 会被并成一个 token。Thai/Khmer/Lao 这类
> 无分词符又不在 CJK 集合内的文字，会整段变成一个 token：比之前完全没有 token 强，
> 但不是分词。
>
> **同分排序**：按 mtime 降序，再按输入顺序（稳定排序），**不按 type**。

**每种类型的特征关键词**：

- `user`：user, preference, background, role, terse
- `feedback`：feedback, rule, avoid, style, summary
- `project`：project, goal, incident, deadline, release
- `reference`：reference, dashboard, ticket, docs, link

**Prompt 构建规则**：

- 单次注入最多 5 篇文档（`MAX_RELEVANT_DOCS`）
- 每篇文档 body 截断至 1200 字符（`MAX_DOC_BODY_CHARS`）
- 超出截断时追加提示："NOTE: Relevant memory truncated for prompt budget."
- 包含文档的新鲜度信息（基于文件 mtime）

> **`MAX_RELEVANT_DOCS` 限制的是单次注入，不是单轮总量。** Fast 阶段投递 2 篇、
> ToolResult 阶段又投递 5 篇全新文档时，本轮进入模型的是 **7 篇**——去重只消除
> 重复，不压缩总和。这是放弃跨阶段预算核算的有意结果（见
> `2026-08-08-native-memory-recall-reliability.md`）：两次 Prompt 各自有界，
> 每篇 body 仍截断到 1200 字符，Fast 上限为 2，因此最坏情况有界且不大，只是不等于 5。

### 投递时机（Delivery）

"选中了 Memory" 不等于 "主模型看到了 Memory"。Recall 在 UserQuery 到达时异步启动，
投递发生在两个时机：

```mermaid
flowchart TD
    A[UserQuery 到达\n启动 Recall Prefetch] --> B{等待结束\n以先到者为准:\nRecall 完成 / Fast 就绪 /\n取消 / 100 ms 上限}
    B --> B1{Recall 是否完成?}
    B1 -- 是 --> C{选中结果非空?}
    C -- 是 --> C1[注入首轮 Prompt\nphase: refined]
    C -- 否 --> C0[丢弃\nno_relevant_results]
    B1 -- 否 --> D{是否有确定性\nFast 结果?}
    D -- 是 --> E[注入首轮 Prompt\nphase: fast\n最多 2 篇 MAX_FAST_RECALL_DOCS]
    D -- 否 --> F[首轮不注入]
    E --> G[Recall 继续运行]
    F --> G
    G --> H{本轮是否有\nToolResult?}
    H -- 是 --> I{Recall 是否已完成?}
    I -- 是 --> J[排除 Fast 已投递文档\n按剩余文档重建 Prompt]
    I -- 否 --> M
    J --> J1{还有剩余文档?}
    J1 -- 是 --> K[注入 ToolResult\nphase: refined]
    J1 -- 否 --> L{Recall 选中了文档?}
    L -- 是 --> L1[丢弃\nalready_delivered]
    L -- 否 --> L2[丢弃\nno_relevant_results]
    H -- 否 --> M{选中文档是否\n全部已被 Fast 投递?}
    M -- 是 --> L1
    M -- 否 --> M1[丢弃\nno_safe_delivery_point]
```

**为什么需要 Fast 阶段**：当存在 Config 时 Recall 会等待 Model Selector，
而它是一次网络 Side Query（中止上限 30 秒），因此 100 ms 预算通常会超时。
若没有 Fast 阶段，**没有工具调用的轮次将完全拿不到 Memory**——而这正是
用户级 Memory 最重要的场景。Fast 结果复用 `selectModelCandidateDocuments`
为 Model Manifest 已经算好的候选，不产生额外扫描或 I/O。

**100 ms 是上限而不是固定开销**：Fast 结果在 Recall 扫完 Memory 树之后才发布，
所以真正决定它能否赶上的是**扫描耗时**，不是打分耗时（后者是微秒级）。
`recall-scan-latency.test.ts` 测量同一 Session 内复用文档缓存后的稳态扫描，
不能证明首次冷缓存扫描在 100 ms 内完成。首次扫描需要读取和解析全部主题文件，
大语料下可能超过上限；此时该轮最多等待 100 ms，但不会投递尚未就绪的 Fast 结果。
后续召回复用文档缓存，Fast 就绪后会立即结束等待，不再继续等待通常更慢的
Model Selector。该上限只约束等待成本，并不保证每个语料规模都能完成首次投递。

**Fast 阶段的边界**：Fast 结果就是确定性结果，因此它只能解决**时机**问题，
解决不了**匹配**问题。与文档没有任何词面重叠的 Query 产生不了 Fast 结果，
这类 Query 在无工具回合仍然拿不到 Memory——只有 Model Selector 能覆盖它们，
而无工具回合等不到 Selector。语料中的 `semantic-no-lexical` 分片专门测量这一点。

**去重**：两个阶段来自同一次扫描，Model Selector 并未把 Fast 文档视为已排除，
因此 ToolResult 投递前必须过滤掉 Fast 已投递的 `filePath` 并重建 Prompt。

**丢弃口径**：同一条规则也适用于取消路径。若最终选中的文档已被 Fast 阶段
全部投递，无论本轮是因为无工具调用、New Query、Reset、Abort 还是 Shutdown
结束，都记为 `already_delivered` 而不是对应的取消原因——否则「Memory 从未
到达模型」这一桶会被实际已送达的回合灌水。只有部分重叠时仍记取消原因，
因为不在 Fast 集合里的那些文档确实没有投递点。

---

## Forget — 遗忘

### 触发时机

由用户手动执行 `/forget <query>` 命令触发。

### 遗忘流程（`forget.ts`）

```mermaid
flowchart TD
    A[forgetManagedAutoMemoryEntries\nquery + config] --> B[ensureAutoMemoryScaffold]
    B --> C[listIndexedForgetCandidates\n扫描所有文件的所有 entry]
    C --> D[为每个 entry 生成稳定 ID\n单 entry 文件: relativePath\n多 entry 文件: relativePath:index]
    D --> E{是否配置了 Config?}
    E -- 是 --> F[selectByModel\n构建 selection prompt\n发起 side query temperature=0]
    F --> G{模型选择成功?}
    G -- 是 --> H[strategy: model]
    G -- 失败 --> I[selectByHeuristic\n关键词匹配]
    E -- 否 --> I
    I --> J[strategy: heuristic]
    H --> K[遍历选中的 candidates]
    J --> K
    K --> L{entries.length == 1?}
    L -- 是 --> M[删除整个文件\nfs.unlink]
    L -- 否 --> N[解析文件中的所有 entries\n移除目标 entry\n重新渲染写回]
    M --> O[记录 removedEntries]
    N --> O
    O --> P{有 touched topics?}
    P -- 是 --> Q[bumpMetadata\n重建 MEMORY.md 索引]
    P --> R[返回 AutoMemoryForgetResult]
    Q --> R
```

**Entry ID 设计**：

- 单条目文件（常见情况）：`relativePath`（如 `feedback/no-summary.md`）
- 多条目文件：`relativePath:index`（如 `feedback/style.md:2`）
- 使用稳定 ID 使模型可以精确定位条目而不影响同文件的其他条目

---

## 索引重建

`MEMORY.md` 是所有主题文件的导航索引，每次 Extract 或 Dream 后调用 `rebuildManagedAutoMemoryIndex` 重建：

```
- [用户偏好](user/preferences.md) — 用户是资深 Go 工程师，第一次接触 React
- [反馈规范](feedback/style.md) — 保持回复简洁，不要尾部总结
- [项目里程碑](project/milestone.md) — 移动端发布切分支前的合并冻结窗口
```

**索引限制**：

- 每行预算 150 字符（`MAX_INDEX_LINE_CHARS`）：标题与描述先各自按字段上限（`MAX_INDEX_FIELD_CHARS`，120 字符）截断；再按整行预算让描述让位——能整段放下就整段保留，否则截到剩余空间，但会被截到不足 `MIN_INDEX_HOOK_CHARS`（24）字符时整条描述省略；`](path)` 链接目标必须完整可解析，链接本身就超过 150 字符时该行允许超长（不截链接）
- 最多 200 行
- 总大小不超过 25,000 字节

---

## 遥测埋点

系统内置三类遥测事件，用于监控记忆操作的性能和效果：

### Extract 遥测

| 字段             | 类型                        | 说明                    |
| ---------------- | --------------------------- | ----------------------- |
| `trigger`        | `'auto'`                    | 触发方式（当前仅自动）  |
| `status`         | `'completed'` \| `'failed'` | 执行结果                |
| `patches_count`  | number                      | 提取到的有效 patch 数量 |
| `touched_topics` | string[]                    | 被写入的记忆类型列表    |
| `duration_ms`    | number                      | 总耗时（毫秒）          |

### Dream 遥测

| 字段              | 类型                                  | 说明                   |
| ----------------- | ------------------------------------- | ---------------------- |
| `trigger`         | `'auto'`                              | 触发方式               |
| `status`          | `'updated'` \| `'noop'` \| `'failed'` | 执行结果               |
| `deduped_entries` | number                                | 机械路径去重的条目数量 |
| `touched_topics`  | string[]                              | 被修改的记忆类型列表   |
| `duration_ms`     | number                                | 总耗时（毫秒）         |

### Recall 遥测

| 字段            | 类型                                   | 说明             |
| --------------- | -------------------------------------- | ---------------- |
| `query_length`  | number                                 | 查询字符串长度   |
| `docs_scanned`  | number                                 | 扫描的文档总数   |
| `docs_selected` | number                                 | 最终注入的文档数 |
| `strategy`      | `'none'` \| `'heuristic'` \| `'model'` | 选择策略         |
| `duration_ms`   | number                                 | 总耗时（毫秒）   |

### Recall Delivery 遥测

记录选中的 Memory 是否真的送达主模型（Selection 事件无法回答这个问题）。

| 字段             | 类型                                                                                                                                      | 说明                                                       |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `phase`          | `'fast'` \| `'refined'`                                                                                                                   | **投递阶段**                                               |
| `delivery_point` | `'initial'` \| `'tool_result'` \| `'discarded'`                                                                                           | 投递位置                                                   |
| `discard_reason` | `'no_safe_delivery_point'` \| `'new_query'` \| `'reset'` \| `'abort'` \| `'shutdown'` \| `'no_relevant_results'` \| `'already_delivered'` | 丢弃原因                                                   |
| `strategy`       | `'none'` \| `'heuristic'` \| `'model'`                                                                                                    | **选择方式**                                               |
| `docs_selected`  | number                                                                                                                                    | 结果文档数（投递事件为实际投递数；discarded 事件为选中数） |
| `latency_ms`     | number                                                                                                                                    | 自发起的耗时                                               |

> **`phase` 与 `strategy` 正交，互不替代。** `phase` 描述**何时**送达：`fast` 是预算
> 超时后注入的确定性结果，`refined` 是 Model Selector 选出的结果。`strategy` 描述
> **如何**选出。`fast` 投递必然是 `heuristic`；`refined` 投递常规为 `model`，
> 在 Selector 失败走 Fallback 时为 `heuristic`。仅凭 `strategy` 判断阶段，
> 会把"确定性结果先到"与"Selector 故障"混为一谈。

---

## 相关源文件索引

| 文件                                                 | 职责                                                                          |
| ---------------------------------------------------- | ----------------------------------------------------------------------------- |
| `packages/core/src/memory/types.ts`                  | 类型定义：`AutoMemoryType`、`AutoMemoryMetadata`、`AutoMemoryExtractCursor`   |
| `packages/core/src/memory/paths.ts`                  | 路径计算：`getAutoMemoryRoot`、`isAutoMemPath`、各类文件路径 helpers          |
| `packages/core/src/memory/store.ts`                  | 脚手架初始化：`ensureAutoMemoryScaffold`，索引/元数据读写                     |
| `packages/core/src/memory/scan.ts`                   | 扫描主题文件：`scanAutoMemoryTopicDocuments`，解析 frontmatter                |
| `packages/core/src/memory/entries.ts`                | 条目解析和渲染：`parseAutoMemoryEntries`、`renderAutoMemoryBody`              |
| `packages/core/src/memory/extract.ts`                | 提取核心逻辑：`runAutoMemoryExtract`，游标管理，patch 去重                    |
| `packages/core/src/memory/extractScheduler.ts`       | 提取调度器：`ManagedAutoMemoryExtractRuntime`，队列/运行状态机                |
| `packages/core/src/memory/extractionAgentPlanner.ts` | 提取 Agent：`runAutoMemoryExtractionByAgent`                                  |
| `packages/core/src/memory/dream.ts`                  | 整合核心逻辑：`runManagedAutoMemoryDream`，Agent 路径 + 机械去重              |
| `packages/core/src/memory/dreamScheduler.ts`         | 整合调度器：`ManagedAutoMemoryDreamRuntime`，门控检查，锁管理                 |
| `packages/core/src/memory/dreamAgentPlanner.ts`      | 整合 Agent：`planManagedAutoMemoryDreamByAgent`                               |
| `packages/core/src/memory/recall.ts`                 | 召回逻辑：`resolveRelevantAutoMemoryPromptForQuery`，启发式+模型双路径        |
| `packages/core/src/memory/forget.ts`                 | 遗忘逻辑：`forgetManagedAutoMemoryEntries`，候选生成+精确删除                 |
| `packages/core/src/memory/indexer.ts`                | 索引重建：`rebuildManagedAutoMemoryIndex`，`buildManagedAutoMemoryIndex`      |
| `packages/core/src/memory/prompt.ts`                 | 系统提示模板：记忆类型说明、格式示例、使用规范                                |
| `packages/core/src/memory/governance.ts`             | 治理建议类型：`AutoMemoryGovernanceSuggestionType`                            |
| `packages/core/src/memory/state.ts`                  | 提取运行状态：`isExtractRunning`、`markExtractRunning`、`clearExtractRunning` |
| `packages/core/src/memory/memoryAge.ts`              | 新鲜度描述：`memoryAge`、`memoryFreshnessText`                                |

---

### Design Adaptive Output Token Escalation Adaptive Output Token Escalation Design (`docs/design/adaptive-output-token-escalation/adaptive-output-token-escalation-design.md`)

# Output Token Limit and Escalation Design

> Defaults to the model's declared output limit unless the user or environment configures `max_tokens`, then uses escalation and multi-turn recovery only when a response still hits `MAX_TOKENS`.

## Problem

Every API request reserves a fixed GPU slot proportional to `max_tokens`. A low default can reduce slot reservation, but it also makes normal large responses more likely to truncate. For file-writing workflows, that can produce incomplete tool-call arguments and force the scheduler to reject the partial write.

## Solution

Use the model's declared output limit by default. When a response is truncated (the model hits `max_tokens`):

1. **Escalate** to the model's full output limit (with 64K as a floor when the current limit is lower)
2. If still truncated, **recover** by keeping the partial response in history and injecting a continuation message, up to 3 times
3. If recovery is exhausted, fall back to the tool scheduler's truncation guidance

This favors correctness for large generation and file-edit tasks. Operators that need a lower reservation can still set `QWEN_CODE_MAX_OUTPUT_TOKENS`, and that explicit value is respected.

## Architecture

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Token limit determination

The effective `max_tokens` is resolved in the following priority order:

| Priority    | Source                                               | Value (known model)          | Value (unknown model)              | Escalation behavior                             |
| ----------- | ---------------------------------------------------- | ---------------------------- | ---------------------------------- | ----------------------------------------------- |
| 1 (highest) | User config (`samplingParams.max_tokens`)            | `min(userValue, modelLimit)` | `userValue`                        | No escalation                                   |
| 2           | Environment variable (`QWEN_CODE_MAX_OUTPUT_TOKENS`) | `min(envValue, modelLimit)`  | `envValue`                         | No escalation                                   |
| 3 (lowest)  | Model/default output limit                           | `modelLimit`                 | `DEFAULT_OUTPUT_TOKEN_LIMIT` = 32K | Escalates to model limit (64K floor) + recovery |

A "known model" is one that has an explicit entry in `OUTPUT_PATTERNS` (checked via `hasExplicitOutputLimit()`). For known models, the effective value is always capped at the model's declared output limit to avoid API errors. Unknown models (custom deployments, self-hosted endpoints) pass the user's value through directly, since the backend may support larger limits.

This logic is implemented in three content generators:

- `DefaultOpenAICompatibleProvider.applyOutputTokenLimit()` — OpenAI-compatible providers
- `DashScopeProvider` — inherits `applyOutputTokenLimit()` from the default provider
- `AnthropicContentGenerator.buildSamplingParameters()` — Anthropic provider

## Escalation mechanism

The escalation logic lives in `geminiChat.ts`, placed **outside** the main retry loop. This is intentional:

1. The retry loop handles transient errors (rate limits, invalid streams, content validation)
2. Truncation is not an error — it's a successful response that was cut short
3. Errors from the escalated stream should propagate directly to the caller, not be caught by retry logic

### Escalation steps (geminiChat.ts)

```
1. Stream completes successfully (lastError === null)
2. Last chunk has finishReason === MAX_TOKENS
3. Guard checks pass:
   - maxTokensEscalated === false (prevent infinite escalation)
   - hasUserMaxTokensOverride === false (respect user intent)
4. Compute escalated limit: max(ESCALATED_MAX_TOKENS, tokenLimit(model, 'output'))
5. Pop the partial model response from chat history
6. Yield RETRY event (isContinuation: false) → UI discards partial output and resets buffers
7. Re-send the same request with maxOutputTokens: escalatedLimit
```

### Recovery steps (geminiChat.ts)

If the escalated response is also truncated (finishReason === MAX_TOKENS), the recovery loop runs up to `MAX_OUTPUT_RECOVERY_ATTEMPTS` (3) times:

```
1. Partial model response is already in history (pushed by processStreamResponse)
2. Push a recovery user message: OUTPUT_RECOVERY_MESSAGE
3. Yield RETRY event (isContinuation: true) → UI keeps text buffer for continuation
4. Re-send with updated history (model sees its partial output + recovery instruction)
5. If still truncated and attempts remain, loop back to step 1
6. If recovery attempt throws (empty response, network error):
   - Pop the dangling recovery message from history
   - Break out of recovery loop
```

### State cleanup on RETRY (turn.ts)

When the `Turn` class receives a RETRY event, it clears accumulated state to prevent inconsistencies:

- `pendingToolCalls` — cleared to avoid duplicate tool calls if the first truncated response contained completed tool calls that are repeated in the escalated response
- `pendingCitations` — cleared to avoid duplicate citations
- `finishReason` — reset to `undefined` so the new response's finish reason is used

The `isContinuation` flag is passed through to the UI so it can decide whether to reset text buffers (escalation) or keep them (recovery).

## Constants

Defined in `geminiChat.ts` and `tokenLimits.ts`:

| Constant                       | Value  | Purpose                                           |
| ------------------------------ | ------ | ------------------------------------------------- |
| `ESCALATED_MAX_TOKENS`         | 64,000 | Floor for escalation when the model limit is low  |
| `MAX_OUTPUT_RECOVERY_ATTEMPTS` | 3      | Max multi-turn recovery attempts after escalation |

The effective escalated limit is `max(ESCALATED_MAX_TOKENS, tokenLimit(model, 'output'))`:

| Model            | Escalated limit |
| ---------------- | --------------- |
| Claude Opus 4.6  | 131,072 (128K)  |
| GPT-5 / o-series | 131,072 (128K)  |
| Qwen3.x          | 65,536 (64K)    |
| Unknown models   | 64,000 (floor)  |

## Design decisions

### Why not use an 8K default?

- An 8K default is a slot-reservation/capacity optimization, not a correctness requirement. It trades correctness (large responses truncate) for backend throughput (a request reserves a GPU slot proportional to `max_tokens`, so a lower value over-reserves less).
- Large file generation and edit tool calls can legitimately exceed 8K, so an 8K default turns a normal request into a truncate → escalate round-trip (and, in the worst case, a retry loop).
- Claude Code keeps the same 8K cap but gates it behind a feature flag (`tengu_otk_slot_v1`) that **defaults to off for third-party providers** ("not validated on Bedrock/Vertex") — i.e. its default behavior for non-first-party serving is exactly "use the model's declared limit." qwen-code's providers are all third-party / OpenAI-compatible / self-hosted, so matching that default-off behavior is the safe choice; assuming the low default is safe for every backend is not.
- The capacity tradeoff is not lost, only made opt-in: operators on a capacity-constrained self-hosted backend can set `QWEN_CODE_MAX_OUTPUT_TOKENS` (e.g. `8000`) to restore the lower per-request reservation. A GrowthBook-style feature flag is intentionally not reintroduced — qwen-code has no such infrastructure, and the env var already covers the need.

### Why escalate to model limit instead of fixed 64K?

- Models with higher output limits (Claude Opus 128K, GPT-5 128K) were constrained to 64K unnecessarily
- Using the model's actual limit captures the vast majority of long outputs without a second retry
- `ESCALATED_MAX_TOKENS` (64K) serves as a floor for unknown models where `tokenLimit()` returns the default 32K

### Why multi-turn recovery instead of progressive escalation?

- Progressive escalation (for example 16K -> 32K -> 64K) requires regenerating the full response each time
- Multi-turn recovery keeps the partial response and lets the model continue, saving tokens and latency
- Recovery messages are cheap (~40 tokens each) compared to regenerating large responses
- The 3-attempt limit prevents infinite loops while covering most practical cases

### Why is escalation outside the retry loop?

- Truncation is a success case, not an error
- Errors from the escalated stream (rate limits, network failures) should propagate directly rather than being silently retried with incorrect parameters
- Keeps the retry loop focused on its original purpose (transient error recovery)
- Recovery errors are caught separately to avoid aborting the entire conversation

---

### Design Prompt Suggestion Prompt Suggestion Design (`docs/design/prompt-suggestion/prompt-suggestion-design.md`)

# Prompt Suggestion (NES) Design

> Predicts what the user would naturally type next after the AI completes a response, showing it as ghost text in the input prompt.
>
> Implementation status: `prompt-suggestion-implementation.md`. Speculation engine: `speculation-design.md`.

## Overview

A **prompt suggestion** (Next-step Suggestion / NES) is a short prediction (2-12 words) of the user's next input, generated by an LLM call after each AI response. It appears as ghost text in the input prompt. The user can accept it with Tab/Enter/Right Arrow or dismiss it by typing.

## Architecture

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Suggestion Generation

### LLM Prompt

```
[SUGGESTION MODE: Suggest what the user might naturally type next.]

FIRST: Read the LAST FEW LINES of the assistant's most recent message — that's where
next-step hints, tips, and actionable suggestions usually appear. Then check the user's
recent messages and original request.

Your job is to predict what THEY would type - not what you think they should do.
THE TEST: Would they think "I was just about to type that"?

PRIORITY: If the assistant's last message contains a tip or hint like "Tip: type X to ..."
or "type X to ...", extract X as the suggestion. These are explicit next-step hints.

EXAMPLES:
Assistant says "Tip: type post comments to publish findings" → "post comments"
Assistant says "type /review to start" → "/review"
User asked "fix the bug and run tests", bug is fixed → "run the tests"
After code written → "try it out"
Task complete, obvious follow-up → "commit this" or "push it"

Format: 2-12 words, match the user's style. Or nothing.
Reply with ONLY the suggestion, no quotes or explanation.
```

### Filter Rules (12)

| Rule               | Example blocked                                  |
| ------------------ | ------------------------------------------------ |
| done               | "done"                                           |
| meta_text          | "nothing found", "no suggestion", "silence"      |
| meta_wrapped       | "(silence)", "[no suggestion]"                   |
| error_message      | "api error: 500"                                 |
| prefixed_label     | "Suggestion: commit"                             |
| too_few_words      | "hmm" (but allows "yes", "commit", "push" etc.)  |
| too_many_words     | > 12 words                                       |
| too_long           | >= 100 chars                                     |
| multiple_sentences | "Run tests. Then commit."                        |
| has_formatting     | newlines, markdown bold                          |
| evaluative         | "looks good", "thanks" (with \b word boundaries) |
| ai_voice           | "Let me...", "I'll...", "Here's..."              |

### Guard Conditions

**AppContainer useEffect (13 checks in code):**

| Guard                | Check                                               |
| -------------------- | --------------------------------------------------- |
| Settings toggle      | `enableFollowupSuggestions`                         |
| Non-interactive      | `config.isInteractive()`                            |
| SDK mode             | `!config.getSdkMode()`                              |
| Streaming transition | `Responding → Idle` (2 checks)                      |
| API error (history)  | `historyManager.history[last]?.type !== 'error'`    |
| API error (pending)  | `!pendingGeminiHistoryItems.some(type === 'error')` |
| Confirmation dialogs | shell + general + loop detection (3 checks)         |
| Permission dialog    | `isPermissionsDialogOpen`                           |
| Elicitation          | `settingInputRequests.length === 0`                 |
| Plan mode            | `ApprovalMode.PLAN`                                 |

**Inside generatePromptSuggestion():**

| Guard              | Check            |
| ------------------ | ---------------- |
| Early conversation | `modelTurns < 2` |

**Separate feature flags (not in guard block):**

| Flag                 | Controls                                                |
| -------------------- | ------------------------------------------------------- |
| `enableCacheSharing` | Whether to use forked query or fallback to generateJson |
| `enableSpeculation`  | Whether to start speculation on suggestion display      |

## State Management

### FollowupState

```typescript
interface FollowupState {
  suggestion: string | null;
  isVisible: boolean;
  shownAt: number; // timestamp for telemetry
}
```

### FollowupController

Framework-agnostic controller shared by CLI (Ink) and WebUI (React):

- `setSuggestion(text)` — 300ms delayed show, null clears immediately
- `accept(method)` — clears state, fires `onAccept` via microtask, 100ms debounce lock
- `dismiss()` — clears state, logs `ignored` telemetry
- `clear()` — hard reset all state + timers
- `Object.freeze(INITIAL_FOLLOWUP_STATE)` prevents accidental mutation

## Keyboard Interaction

| Key         | CLI                         | WebUI                                |
| ----------- | --------------------------- | ------------------------------------ |
| Tab         | Fill input (no submit)      | Fill input (no submit)               |
| Enter       | Fill + submit               | Fill + submit (`explicitText` param) |
| Right Arrow | Fill input (no submit)      | Fill input (no submit)               |
| Typing      | Dismiss + abort speculation | Dismiss                              |
| Paste       | Dismiss + abort speculation | Dismiss                              |

### Key Binding Note

The Tab handler uses `key.name === 'tab'` explicitly (not `ACCEPT_SUGGESTION` matcher) because `ACCEPT_SUGGESTION` also matches Enter, which must fall through to the SUBMIT handler.

## Telemetry

### PromptSuggestionEvent

| Field                      | Type                        | Description                         |
| -------------------------- | --------------------------- | ----------------------------------- |
| outcome                    | accepted/ignored/suppressed | Final outcome                       |
| prompt_id                  | string                      | Default: 'user_intent'              |
| accept_method              | tab/enter/right             | How user accepted                   |
| time_to_accept_ms          | number                      | Time from shown to accept           |
| time_to_ignore_ms          | number                      | Time from shown to dismiss          |
| time_to_first_keystroke_ms | number                      | Time to first keystroke while shown |
| suggestion_length          | number                      | Character count                     |
| similarity                 | number                      | 1.0 for accept, 0.0 for ignore      |
| was_focused_when_shown     | boolean                     | Terminal had focus                  |
| reason                     | string                      | For suppressed: filter rule name    |

### SpeculationEvent

| Field                    | Type                    | Description               |
| ------------------------ | ----------------------- | ------------------------- |
| outcome                  | accepted/aborted/failed | Speculation result        |
| turns_used               | number                  | API round-trips           |
| files_written            | number                  | Files in overlay          |
| tool_use_count           | number                  | Tools executed            |
| duration_ms              | number                  | Wall-clock time           |
| boundary_type            | string                  | What stopped speculation  |
| had_pipelined_suggestion | boolean                 | Next suggestion generated |

## Feature Flags and Settings

| Setting                     | Type    | Default | Description                                                                      |
| --------------------------- | ------- | ------- | -------------------------------------------------------------------------------- |
| `enableFollowupSuggestions` | boolean | true    | Master toggle for prompt suggestions                                             |
| `enableCacheSharing`        | boolean | true    | Use cache-aware forked queries                                                   |
| `enableSpeculation`         | boolean | false   | Predictive execution engine                                                      |
| `fastModel` (top-level)     | string  | ""      | Model for all background tasks (empty = use main model). Set via `/model --fast` |

### Internal Prompt ID Filtering

Background operations use dedicated prompt IDs (`INTERNAL_PROMPT_IDS` in `utils/internalPromptIds.ts`) to prevent their API traffic and tool calls from appearing in the user-visible UI:

| Prompt ID           | Used by                    |
| ------------------- | -------------------------- |
| `prompt_suggestion` | Suggestion generation      |
| `forked_query`      | Cache-aware forked queries |
| `speculation`       | Speculation engine         |

**Filtering applied:**

- `loggingContentGenerator` — skips `logApiRequest` and OpenAI interaction logging for internal IDs
- `logApiResponse` / `logApiError` — skips `chatRecordingService.recordUiTelemetryEvent`
- `logToolCall` — skips `chatRecordingService.recordUiTelemetryEvent`
- `uiTelemetryService.addEvent` — **not filtered** (ensures `/stats` token tracking works)

### Thinking Mode

Thinking/reasoning is explicitly disabled (`thinkingConfig: { includeThoughts: false }`) for all background task paths:

- **Forked query path** (`createForkedChat`) — overrides `thinkingConfig` in the cloned `generationConfig`, covering both suggestion generation and speculation
- **BaseLlm fallback path** (`generateViaBaseLlm`) — per-request config overrides base content generator's thinking settings

This is safe because:

- Cache prefix is determined by systemInstruction + tools + history, not `thinkingConfig` — cache hits are unaffected
- All backends (Gemini, OpenAI-compatible, Anthropic) handle `includeThoughts: false` by omitting the thinking field — no API errors on models without thinking support
- Suggestion generation and speculation don't benefit from reasoning tokens

---

### Design Prompt Suggestion Prompt Suggestion Implementation (`docs/design/prompt-suggestion/prompt-suggestion-implementation.md`)

# Prompt Suggestion Implementation Status

> Tracks the implementation status of the prompt suggestion (NES) feature across all packages.

## Core Module (`packages/core/src/followup/`)

| Component                | Status  | Lines | Description                                                   |
| ------------------------ | ------- | ----- | ------------------------------------------------------------- |
| `followupState.ts`       | ✅ Done | ~230  | Framework-agnostic controller with timer/debounce             |
| `suggestionGenerator.ts` | ✅ Done | ~260  | LLM generation + 12 filter rules + forked query support       |
| `forkedQuery.ts`         | ✅ Done | ~240  | CacheSafeParams + createForkedChat + runForkedQuery           |
| `overlayFs.ts`           | ✅ Done | ~140  | Copy-on-write overlay filesystem                              |
| `speculationToolGate.ts` | ✅ Done | ~150  | Tool boundary enforcement with AST shell parser               |
| `speculation.ts`         | ✅ Done | ~540  | Speculation engine with pipelined suggestion + model override |

## CLI Integration (`packages/cli/`)

| Component                    | Status  | Description                                                |
| ---------------------------- | ------- | ---------------------------------------------------------- |
| `AppContainer.tsx`           | ✅ Done | Suggestion generation, speculation lifecycle, UI rendering |
| `InputPrompt.tsx`            | ✅ Done | Tab/Enter/Right Arrow acceptance, dismiss + abort          |
| `Composer.tsx`               | ✅ Done | Props threading                                            |
| `UIStateContext.tsx`         | ✅ Done | promptSuggestion + dismissPromptSuggestion                 |
| `useFollowupSuggestions.tsx` | ✅ Done | React hook with telemetry + keystroke tracking             |
| `settingsSchema.ts`          | ✅ Done | 3 feature flags + fastModel setting                        |
| `settings.schema.json`       | ✅ Done | VSCode settings schema                                     |

## WebUI Integration (`packages/webui/`)

| Component                   | Status  | Description                                 |
| --------------------------- | ------- | ------------------------------------------- |
| `InputForm.tsx`             | ✅ Done | Tab/Enter/Right Arrow + explicitText submit |
| `useFollowupSuggestions.ts` | ✅ Done | React hook with onOutcome support           |
| `followup.ts`               | ✅ Done | Subpath entry                               |
| `components.css`            | ✅ Done | Ghost text styling                          |
| `vite.config.followup.ts`   | ✅ Done | Separate build config                       |

## Telemetry (`packages/core/src/telemetry/`)

| Component               | Status  | Description          |
| ----------------------- | ------- | -------------------- |
| `PromptSuggestionEvent` | ✅ Done | 10 fields            |
| `SpeculationEvent`      | ✅ Done | 7 fields             |
| `logPromptSuggestion()` | ✅ Done | OpenTelemetry logger |
| `logSpeculation()`      | ✅ Done | OpenTelemetry logger |

## Test Coverage

| Test File                     | Tests | Description                                                     |
| ----------------------------- | ----- | --------------------------------------------------------------- |
| `followupState.test.ts`       | 14    | Controller timer, debounce, accept callback, onOutcome, clear   |
| `suggestionGenerator.test.ts` | 16    | All 12 filter rules + edge cases + false positives              |
| `overlayFs.test.ts`           | 15    | COW write, read resolution, apply, cleanup, path traversal      |
| `speculationToolGate.test.ts` | 27    | Tool categories, approval mode, shell AST, path rewrite         |
| `forkedQuery.test.ts`         | 6     | Cache params save/get/clear, deep clone, version detection      |
| `speculation.test.ts`         | 7     | ensureToolResultPairing edge cases                              |
| `smoke.test.ts`               | 21    | Cross-module E2E: filter + overlay + toolGate + cache + pairing |
| `InputPrompt.test.tsx`        | 4     | Tab, Enter+submit, Right Arrow, completion guard                |

## Audit History

| Round           | Issues Found | Issues Fixed                                             |
| --------------- | ------------ | -------------------------------------------------------- |
| R1-R4           | 10           | 10 (rule engine → LLM, state simplification)             |
| R5-R6           | 2            | 2 (Enter keybinding conflict, Right Arrow telemetry)     |
| R7-R8           | 3            | 3 (WebUI telemetry, dead type, test coverage)            |
| R9              | 0            | — (convergence)                                          |
| R10-R11         | 1            | 1 (historyManager dep)                                   |
| R12-R13         | 1            | 1 (evaluative regex word boundaries)                     |
| Phase 1+2 R1-R4 | 20+          | 20+ (permission bypass, overlay safety, race conditions) |
| **Total**       | **37+**      | **37+**                                                  |

## Claude Code Alignment

| Feature                          | Alignment | Notes                                 |
| -------------------------------- | --------- | ------------------------------------- |
| Prompt text                      | 100%      | Identical (brand name only)           |
| 12 filter rules                  | 100%+     | \b word boundaries improvement        |
| UI interaction (Tab/Enter/Right) | 100%      |                                       |
| Guard conditions                 | 100%      | 13 checks                             |
| Telemetry                        | 100%      | 10+7 fields                           |
| Cache sharing                    | ✅        | DashScope cache_control               |
| Speculation                      | ✅        | COW overlay + tool gating             |
| Pipelined suggestion             | ✅        | Generated after speculation completes |
| State management                 | 100%+     | Controller pattern, Object.freeze     |

---

### Design Prompt Suggestion Speculation Design (`docs/design/prompt-suggestion/speculation-design.md`)

# Speculation Engine Design

> Speculatively executes the accepted suggestion before the user confirms, using copy-on-write file isolation. Results appear instantly when the user presses Tab.

## Overview

When a prompt suggestion is shown, the **speculation engine** immediately starts executing it in the background using a forked GeminiChat. File writes go to a temporary overlay directory. If the user accepts the suggestion, overlay files are copied to the real filesystem and the speculated conversation is injected into the main chat history. If the user types something else, the speculation is aborted and the overlay is cleaned up.

## Architecture

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Copy-on-Write Overlay

```
Real CWD: /home/user/project/
Overlay:  /tmp/qwen-speculation/12345/a1b2c3d4/

Write to src/app.ts:
  1. Copy /home/user/project/src/app.ts → overlay/src/app.ts (first time only)
  2. Tool writes to overlay/src/app.ts

Read from src/app.ts:
  - If in writtenFiles → read from overlay/src/app.ts
  - Otherwise → read from /home/user/project/src/app.ts

New file (src/new.ts):
  - Create overlay/src/new.ts directly (no original to copy)

Accept:
  - copyFile(overlay/src/app.ts → /home/user/project/src/app.ts)
  - copyFile(overlay/src/new.ts → /home/user/project/src/new.ts)
  - rm -rf overlay/

Abort:
  - rm -rf overlay/
```

## Tool Gate Security

| Tool                                                       | Action   | Condition                                    |
| ---------------------------------------------------------- | -------- | -------------------------------------------- |
| read_file, grep, glob, ls, lsp                             | allow    | Read paths resolved through overlay          |
| edit, write_file                                           | redirect | Only in auto-edit / yolo approval mode       |
| edit, write_file                                           | boundary | In default / plan approval mode              |
| shell                                                      | allow    | `isShellCommandReadOnlyAST()` returns true   |
| shell                                                      | boundary | Non-read-only commands                       |
| web_fetch, web_search                                      | boundary | Network requests require user consent        |
| agent, skill, memory, ask_user, todo_write, exit_plan_mode | boundary | Cannot interact with user during speculation |
| Unknown / MCP tools                                        | boundary | Safe default                                 |

### Path Rewrite

- **Write tools**: `rewritePathArgs()` redirects `file_path` to overlay via `overlayFs.redirectWrite()`
- **Read tools**: `resolveReadPaths()` redirects `file_path` to overlay via `overlayFs.resolveReadPath()` if previously written
- **Rewrite failure**: Treated as boundary (e.g., absolute path outside cwd throws in `redirectWrite`)

## Boundary Handling

When a boundary is hit mid-turn:

1. Already-executed tool calls are preserved (index-based tracking, not name-based)
2. Unexecuted function calls are stripped from the model message
3. Partial tool responses are added to history
4. `ensureToolResultPairing()` validates completeness before injection

## Pipelined Suggestion

After speculation completes (no boundary), a second LLM call generates the **next** suggestion:

```
Context: original conversation + "commit this" + speculated messages
→ LLM predicts: "push it"
→ Stored in state.pipelinedSuggestion
→ On accept: setPromptSuggestion("push it") — appears instantly
```

This enables Tab-Tab-Tab workflows where each acceptance immediately shows the next step.

The pipelined suggestion reuses the exported `SUGGESTION_PROMPT` constant from `suggestionGenerator.ts` (not a local copy) to ensure consistent quality with initial suggestions.

## Fast Model

`startSpeculation` accepts an optional `options.model` parameter, threaded through `runSpeculativeLoop` and `generatePipelinedSuggestion` to `runForkedQuery`. Configured via the top-level `fastModel` setting (empty = use main model). The same `fastModel` is used for all background tasks: suggestion generation, speculation, and pipelined suggestions. Set via `/model --fast <name>` or `settings.json`.

## UI Rendering

When speculation completes, `acceptSpeculation` renders results via `historyManager.addItem()`:

- **User messages**: rendered as `type: 'user'` items
- **Model text**: rendered as `type: 'gemini'` items
- **Tool calls**: rendered as `type: 'tool_group'` items with structured `IndividualToolCallDisplay` entries (tool name, argument description, result text, status)

This shows the user the full speculation output including tool call details, not just plain text.

## Forked Query (Cache Sharing)

### CacheSafeParams

```typescript
interface CacheSafeParams {
  generationConfig: GenerateContentConfig; // systemInstruction + tools
  history: Content[]; // curated, max 40 entries
  model: string;
  version: number; // increments on config changes
}
```

- Saved after each successful main turn in `GeminiClient.sendMessageStream()`
- Cleared on `startChat()` / `resetChat()` to prevent cross-session leakage
- History truncated to 40 entries; `createForkedChat` uses shallow copies (params are already deep-cloned snapshots)
- Thinking mode explicitly disabled (`thinkingConfig: { includeThoughts: false }`) — reasoning tokens are not needed for speculation and would waste cost/latency. This does not affect cache prefix matching (determined by systemInstruction + tools + history only)
- Version detection via `JSON.stringify` comparison of systemInstruction + tools

### Cache Mechanism

DashScope already enables prefix caching via:

- `X-DashScope-CacheControl: enable` header
- `cache_control: { type: 'ephemeral' }` annotations on messages and tools

The forked `GeminiChat` uses identical `generationConfig` (including tools) and history prefix, so DashScope's existing cache mechanism produces cache hits automatically.

## Constants

| Constant                 | Value | Description                              |
| ------------------------ | ----- | ---------------------------------------- |
| MAX_SPECULATION_TURNS    | 20    | Maximum API round-trips                  |
| MAX_SPECULATION_MESSAGES | 100   | Maximum messages in speculated history   |
| SUGGESTION_DELAY_MS      | 300   | Delay before showing suggestion          |
| ACCEPT_DEBOUNCE_MS       | 100   | Debounce lock for rapid accepts          |
| MAX_HISTORY_FOR_CACHE    | 40    | History entries saved in CacheSafeParams |

## File Structure

```
packages/core/src/followup/
├── followupState.ts          # Framework-agnostic state controller
├── suggestionGenerator.ts    # LLM-based suggestion generation + 12 filter rules
├── forkedQuery.ts            # Cache-aware forked query infrastructure
├── overlayFs.ts              # Copy-on-write overlay filesystem
├── speculationToolGate.ts    # Tool boundary enforcement
├── speculation.ts            # Speculation engine (start/accept/abort)
└── index.ts                  # Module exports
```

---

### Design Session Recap Session Recap Design (`docs/design/session-recap/session-recap-design.md`)

# Session Recap Design

> A brief (1-2 sentence) "where did I leave off" summary surfaced when the user
> returns to an idle session, either on demand (`/recap`) or after the
> terminal has been blurred for 5+ minutes.

## Overview

When a user `/resume`s an old session days later, scrolling back through
pages of history to remember **what they were doing and what came next**
is a real friction point. Just reloading messages does not solve this
UX problem.

The goal is to proactively surface a brief 1-2 sentence recap when the user
returns:

- **High-level task** (what they are doing) → **next step** (what to do next).
- Visually distinct from real assistant replies, so it is never mistaken
  for new model output.
- **Best-effort**: failures must be silent and never break the main flow.

## Triggers

| Trigger         | Conditions                                                                                   | Implementation                                                                                                                                     |
| --------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Manual**      | User runs `/recap`                                                                           | `recapCommand.ts` calls the same underlying service                                                                                                |
| **Auto**        | Terminal blurred (DECSET 1004 focus protocol) for ≥ 5 min + focus returns + stream is `Idle` | `useAwaySummary.ts` — 5min blur timer + `useFocus` event listener                                                                                  |
| **Daemon HTTP** | Remote client calls `POST /session/:id/recap`                                                | `server.ts` route → `bridge.generateSessionRecap` (ext-method roundtrip) → `acpAgent.ts` calls `generateSessionRecap(session.getConfig(), signal)` |

All three paths funnel into the same `generateSessionRecap()` function
in `core/services/sessionRecap.ts` to guarantee identical behavior. The
auto-trigger is gated by `general.showSessionRecap` (default: off —
explicit opt-in, so ambient LLM calls are never silently added to a
user's bill); the manual command and daemon HTTP route ignore that
setting (the caller is making an explicit request).

### Daemon access path

The daemon route is non-strict-gated (mirrors `/session/:id/prompt`'s
posture — recap costs tokens but mutates no state). Capability tag
`session_recap` advertises the route on `/capabilities.features`. SDK
helpers: `DaemonClient.recapSession(sessionId, opts)` and
`DaemonSessionClient.recap(opts)`. See
`docs/developers/qwen-serve-protocol.md` § `POST /session/:id/recap`
for the wire contract and error envelope.

Cancellation is **absent in v1**. The route does not listen for HTTP
client disconnect, no `AbortSignal` is threaded into
`bridge.generateSessionRecap`, and the ACP child handler passes a
never-aborting `AbortController().signal` to the core helper (no
cross-process abort plumbing yet). The only ceilings are the bridge's
60s `SESSION_RECAP_TIMEOUT_MS` backstop and the transport-closed race
against ACP channel death. Wiring an HTTP-side AbortController in
isolation would be cosmetic — the child-side LLM call would still run
to completion, so e2e cancel is not achievable without the cross-
process abort piece. This is acceptable for v1 because recap is short
(single-attempt side-query, `maxOutputTokens: 300`, ~1–5s typical).
A future request-id-based cancel ext-method can plumb full end-to-end
cancellation if/when the bandwidth cost justifies it.

## Architecture

```
/* Detailed source-code truncated for AI context efficiency. */
```

### Files

| File                                                         | Responsibility                                                                   |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `packages/core/src/services/sessionRecap.ts`                 | One-shot LLM call + history filter + tag extraction                              |
| `packages/cli/src/ui/hooks/useAwaySummary.ts`                | Auto-trigger React hook                                                          |
| `packages/cli/src/ui/commands/recapCommand.ts`               | `/recap` manual entry point                                                      |
| `packages/cli/src/ui/components/messages/StatusMessages.tsx` | `AwayRecapMessage` renderer (`※` + bold `recap:` + italic content, all dim)      |
| `packages/cli/src/ui/types.ts`                               | `HistoryItemAwayRecap` type                                                      |
| `packages/cli/src/ui/components/HistoryItemDisplay.tsx`      | Dispatches `away_recap` history items to the renderer                            |
| `packages/cli/src/config/settingsSchema.ts`                  | `general.showSessionRecap` + `general.sessionRecapAwayThresholdMinutes` settings |

## Prompt Design

### System Prompt

`generationConfig.systemInstruction` replaces the main agent's system
prompt for this single call, so the model behaves only as a recap
generator and not as a coding assistant.

Note that `GeminiClient.generateContent()` internally runs the prompt
through `getCustomSystemPrompt()`, which appends the user's memory
(QWEN.md / managed auto-memory) as a suffix. The final system prompt is
therefore `recap prompt + user memory` — useful project context for the
recap, not a leak.

Bullets below correspond 1:1 with `RECAP_SYSTEM_PROMPT`:

- Under 40 words, 1-2 plain sentences (no markdown / lists / headings). For Chinese, treat the budget as roughly 80 characters total.
- First sentence: the high-level task. Then: the concrete next step.
- Explicitly forbid: listing what was done, reciting tool calls, status reports.
- Match the dominant language of the conversation (English or Chinese).
- Wrap output in `<recap>...</recap>`; nothing outside the tags.

### Structured Output + Extraction

The model is instructed to wrap its answer in `<recap>...</recap>`:

```
<recap>Refactoring loopDetectionService.ts to address long-session OOM. Next step is to implement option B.</recap>
```

Why: some models (GLM family, reasoning models) write a "thinking"
paragraph before the final answer. Returning the raw text would leak
that reasoning into the UI.

`extractRecap()` has three fallback tiers:

1. Both tags present: take what is between `<recap>...</recap>` (preferred).
2. Only the open tag (e.g. `maxOutputTokens` truncated the close tag):
   take everything after the open tag.
3. Tag missing entirely: return empty string → service returns `null`
   → UI renders nothing.

The third tier is "skip rather than show the wrong thing" — surfacing
the model's reasoning preamble is worse than showing no recap at all.

### Call Parameters

| Parameter           | Value                          | Reason                                                |
| ------------------- | ------------------------------ | ----------------------------------------------------- |
| `model`             | `getFastModel() ?? getModel()` | Recap doesn't need a frontier model                   |
| `tools`             | `[]`                           | One-shot query, no tool use                           |
| `maxOutputTokens`   | `300`                          | Headroom for 1-2 short sentences + tags               |
| `temperature`       | `0.3`                          | Mostly deterministic, with a bit of natural variation |
| `systemInstruction` | The recap-only prompt above    | Replaces the main agent's role definition             |

## History Filtering

`geminiClient.getChat().getHistory()` returns a `Content[]` that
includes:

- `user` / `model` text messages
- `model` `functionCall` parts
- `user` `functionResponse` parts (which can hold full file contents)
- `model` thought parts (`part.thought` / `part.thoughtSignature`,
  the model's hidden reasoning)

`filterToDialog()` keeps only `user` / `model` parts that have **non-empty
text and are not thoughts**. Two reasons:

- **Tool calls / responses**: a single `functionResponse` can be 10K+
  tokens. 30 such messages would drown the recap LLM in irrelevant
  detail, both wasting tokens and biasing the recap toward
  implementation noise like "called X tool to read Y file".
- **Thought parts**: carry the model's internal reasoning. Including
  them risks treating hidden chain-of-thought as dialogue and
  surfacing it in the recap text.

After dropping empty messages, `takeRecentDialog` slices to the last 30
messages and refuses to start the slice on a dangling model/tool
response.

## Concurrency and Edge Cases

### Auto-trigger hook state machine

`useAwaySummary` keeps three refs:

| Ref               | Meaning                                           |
| ----------------- | ------------------------------------------------- |
| `blurredAtRef`    | Blur start time (not cleared until focus returns) |
| `recapPendingRef` | Whether an LLM call is in flight                  |
| `inFlightRef`     | The current in-flight `AbortController`           |

`useEffect` deps: `[enabled, config, isFocused, isIdle, addItem, thresholdMs]`.

| Event                                                            | Action                                                                                                                                 |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `!enabled \|\| !config`                                          | Abort in-flight call + clear `inFlightRef` + clear `blurredAtRef`                                                                      |
| `!isFocused` and `blurredAtRef === null`                         | Set `blurredAtRef = Date.now()`                                                                                                        |
| `isFocused` and `blurredAtRef === null`                          | Return early (no blur cycle to handle — first render or right after a brief-blur reset)                                                |
| `isFocused` and blur duration < 5 min                            | Clear `blurredAtRef`, wait for next blur cycle                                                                                         |
| `isFocused` and blur ≥ 5 min and `recapPendingRef`               | Return (dedupe)                                                                                                                        |
| `isFocused` and blur ≥ 5 min and `!isIdle`                       | **Preserve** `blurredAtRef` and wait for the turn to finish (`isIdle` is in the deps, so the effect re-fires when streaming completes) |
| `isFocused` and blur ≥ 5 min and `shouldFireRecap` returns false | Clear `blurredAtRef` and return — conversation hasn't moved enough since the last recap (≥ 2 user turns required, mirrors Claude Code) |
| `isFocused` and all conditions met                               | Clear `blurredAtRef`, set `recapPendingRef = true`, create `AbortController`, send the LLM request                                     |

The `.then` callback **re-checks** `isIdleRef.current`: if the user has
started a new turn while the LLM was running, the late-arriving recap
is dropped to avoid inserting it mid-turn.

The `.finally` clears `recapPendingRef`, and clears `inFlightRef` only
if `inFlightRef.current === controller` (so it doesn't overwrite a
newer controller).

A second `useEffect` aborts the in-flight controller on unmount.

### `/recap` gating

`CommandContext.ui.isIdleRef` exposes the current stream state
(mirroring the existing `btwAbortControllerRef` pattern). In
interactive mode, `recapCommand` refuses when `!isIdleRef.current`
**or** `pendingItem !== null`. `pendingItem` alone is insufficient
because a normal model reply runs with `streamingState === Responding`
and a null `pendingItem`.

## Configuration and Model Selection

### User-facing knobs

| Setting                                    | Default | Notes                                                                               |
| ------------------------------------------ | ------- | ----------------------------------------------------------------------------------- |
| `general.showSessionRecap`                 | `false` | Auto-trigger only. Manual `/recap` ignores this.                                    |
| `general.sessionRecapAwayThresholdMinutes` | `5`     | Minutes blurred before auto-recap fires on focus-in. Matches Claude Code's default. |
| `fastModel`                                | unset   | Recommended (e.g. `qwen3-coder-flash`) for fast and cheap recaps.                   |

### Model fallback

`config.getFastModel() ?? config.getModel()`:

- User has a `fastModel` set and it is valid for the current auth type
  → use `fastModel`.
- Otherwise → fall back to the main session model (works, just costlier
  and slower).

## Observability

`createDebugLogger('SESSION_RECAP')` emits:

- caught exceptions from the recap path (`debugLogger.warn`).

All failures are **fully transparent** to the user — recap is an
auxiliary feature and never throws into the UI. Developers can grep for
the `[SESSION_RECAP]` tag in the debug log file: written by default to
`~/.qwen/debug/<sessionId>.txt` (`latest.txt` symlinks to the current
session); disable via `QWEN_DEBUG_LOG_FILE=0`.

## Out of Scope

| Item                                             | Why not                                                                                                                                  |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Progress UI for `/recap` (spinner / pendingItem) | 3-5 second wait is tolerable; adds complexity.                                                                                           |
| Automated tests                                  | Service is small (~150 lines), end-to-end tested manually first; unit tests can land in a separate PR.                                   |
| Localized prompts                                | The system prompt is for the model; English is the most reliable substrate. The model selects the output language from the conversation. |
| `QWEN_CODE_ENABLE_AWAY_SUMMARY` env var          | Claude Code uses it to keep the feature on when telemetry is disabled; Qwen Code's current telemetry model doesn't need this.            |
| Auto-recap on `/resume` completion               | A natural follow-up but needs a hook point in `useResumeCommand`; out of scope for this PR.                                              |

---

### Design Session Title Session Title Design (`docs/design/session-title/session-title-design.md`)

# Session Title Design

> A 3-7 word sentence-case session title generated by the fast model after
> the first assistant turn. Persisted in the session JSONL with a
> `titleSource: 'auto' | 'manual'` tag, surfaced in the session picker,
> and regeneratable on demand via `/rename --auto`.

## Overview

`/rename` (#3093) lets a user label a session so they can find it again in
the picker later, but until they run it the picker shows the first user
prompt — often truncated mid-sentence, or describing a framing question
rather than what the session actually became about. Manual renaming is
optional friction most users never do.

The goal is to make session names _useful by default_:

- **Descriptive** of what the session actually accomplished, not just the
  opening line. 3-7 words, sentence case, git-commit-subject style.
- **Best-effort**: fires in the background after the first reply; if it
  fails the user never sees an error.
- **Deferential to the user**: never clobber a `/rename` title the user
  chose deliberately, even across CLI tabs on the same session.
- **Explicitly regeneratable** via `/rename --auto` for the "auto title
  became stale / I want a fresh one" case.

## Triggers

| Trigger    | Conditions                                                                                                                                                                                                    | Implementation                                                 |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Auto**   | After `recordAssistantTurn` fires. Skipped if an existing or pending explicit title is set, recording has failed, another attempt is in-flight, cap reached, non-interactive, env disabled, or no fast model. | `ChatRecordingService.maybeTriggerAutoTitle` — fire-and-forget |
| **Manual** | User runs `/rename --auto`                                                                                                                                                                                    | `renameCommand.ts` via `tryGenerateSessionTitle`               |

Both paths funnel into a single function — `tryGenerateSessionTitle(config,
signal)` — to guarantee identical prompt, schema, model selection, and
sanitization. The auto trigger is a best-effort background call; the
manual `/rename --auto` is a blocking user action that surfaces a
reason-specific error on failure.

## Architecture

```
/* Detailed source-code truncated for AI context efficiency. */
```

### Files

| File                                                 | Responsibility                                                                     |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `packages/core/src/services/sessionTitle.ts`         | One-shot LLM call + history filter + sanitize. Exports `tryGenerateSessionTitle`.  |
| `packages/core/src/services/chatRecordingService.ts` | `maybeTriggerAutoTitle` trigger, guards, cross-process re-read, abort-on-finalize. |
| `packages/core/src/services/sessionService.ts`       | `getSessionTitleInfo` public accessor; `renameSession` accepts `titleSource`.      |
| `packages/core/src/utils/sessionStorageUtils.ts`     | `extractLastJsonStringFields` + `readLastJsonStringFieldsSync` atomic pair reader. |
| `packages/core/src/utils/terminalSafe.ts`            | `stripTerminalControlSequences` shared by sentence-case and kebab paths.           |
| `packages/cli/src/ui/commands/renameCommand.ts`      | `/rename --auto`, sentinel parser, failure-reason message map.                     |
| `packages/cli/src/ui/components/SessionPicker.tsx`   | Dim styling for `titleSource === 'auto'`.                                          |

## Prompt Design

### System Prompt

Replaces the main agent's system prompt for this single call so the model
only tries to label the session, not behave as a coding assistant.

Bullets below correspond 1:1 with `TITLE_SYSTEM_PROMPT`:

- 3-7 words, sentence case (only first word and proper nouns capitalized).
- No trailing punctuation, no markdown, no quotes.
- Match the dominant language of the conversation; for Chinese, budget
  roughly 12-20 characters.
- Be specific about the user's actual goal — name the feature, bug, or
  subject area. Avoid vague catch-alls like "Code changes" or "Help
  request".
- Four good examples (three English + one Chinese) and four bad examples
  (too vague / too long / wrong case / trailing punctuation).
- Return only a JSON object with a single `title` key.

### Structured Output (JSON schema)

Instead of wrapping output in tags (as session-recap does), we use
`BaseLlmClient.generateJson` with a function-calling schema:

```ts
const TITLE_SCHEMA = {
  type: 'object',
  properties: {
    title: {
      type: 'string',
      description:
        'A concise sentence-case session title, 3-7 words, no trailing punctuation.',
    },
  },
  required: ['title'],
};
```

Why function calling rather than free text + tag extraction:

1. Cross-provider reliability — OpenAI-compatible endpoints, Gemini, and
   Qwen's native tool-calling all implement function calling; tag parsing
   would rely on every model respecting a text convention.
2. No reasoning-preamble leakage — the function call arguments come back
   structured, so a "thinking" paragraph before the answer can't bleed
   into the title.
3. Simpler post-processing — a single `typeof result.title === 'string'`
   check plus `sanitizeTitle` covers every realistic model drift.

The model may still return something the schema allows but the UX
rejects (empty string, whitespace-only, 500 chars, markdown fencing,
control chars). `sanitizeTitle` handles all of these and returns `''` →
service returns `{ok: false, reason: 'empty_result'}`.

### Call Parameters

| Parameter         | Value                          | Reason                                                                                          |
| ----------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- |
| `model`           | `getFastModel()` — no fallback | Auto-titling on main-model tokens is too expensive to be silent.                                |
| `schema`          | `TITLE_SCHEMA`                 | Forces `{title: string}`; filters shape drift at the transport layer.                           |
| `maxOutputTokens` | `100`                          | More than enough for 7 words plus schema overhead.                                              |
| `temperature`     | `0.2`                          | Mostly deterministic — session titles benefit from stability across regeneration.               |
| `maxAttempts`     | `1`                            | Titles are best-effort cosmetic metadata; retries would queue behind user-visible main traffic. |

Contrast with session-recap, which falls back to the main model. Title
generation is triggered automatically and often; silently spending
main-model tokens without a user opt-in is a real bill surprise. Manual
`/rename --auto` explicitly fails with `no_fast_model` rather than
fallback — forcing the user to make the fast-model choice consciously.

## History Filtering

`geminiClient.getChat().getHistory()` returns `Content[]` that includes
tool calls, tool responses (often 10K+ tokens of file content), and model
thought parts. Feeding that raw into the title LLM would bias the label
toward implementation noise like "Called grep on auth module".

`filterToDialog` keeps only `user` / `model` entries with non-empty text
and no `thought` / `thoughtSignature` parts. `takeRecentDialog` slices to
the last 20 messages and refuses to start on a dangling model/tool
response. `flattenToTail` converts to "Role: text" lines and slices the
last 1000 characters.

### The 1000-character tail slice

A session that starts with `help me debug X` but pivots to refactoring Y
should be titled about Y. Titling by the head locks in the opening
framing; titling by the tail captures what the session became.

### UTF-16 surrogate handling

`.slice(-1000)` on a UTF-16 code-unit boundary can orphan a high or low
surrogate if a CJK supplementary char or emoji gets cut. Some providers
respond to the resulting invalid UTF-16 with a 400 — which, without
handling, would burn an attempt for no reason. `flattenToTail` drops a
leading orphaned low surrogate; `sanitizeTitle` scrubs any orphaned
surrogate after the max-length trim on the output path too.

## Persistence

### Durable result contract

`ChatRecordingService.recordCustomTitle()` returns `Promise<boolean>`.
`true` means the `custom_title` record has completed the recorder's ordered
JSONL write, the in-memory title and source have been updated, and the title
observer has been notified. It does not mean merely accepted into the write
queue. A file-creation failure, an existing recorder failure, an earlier
queued write failure, or the title write's own rejection returns `false` and
leaves the prior title and observer state unchanged. The recorder's canonical
`flush()` still reports the sticky writer failure.

The observer runs only after persistence. Observer exceptions are isolated
because an already written title cannot be retroactively classified as a
failed rename. The callback also receives the session ID captured in the
persisted record; consumers must not read the mutable Config session at callback
time. This prevents a delayed write from an outgoing recorder from renaming the
new prompt bar after `/clear`, resume, or branch switches sessions.

Background auto-title uses the same private persistence path but remains
best-effort. Public calls are explicit title requests, including
`recordCustomTitle(title, 'auto')` from `/rename --auto` and remote control
surfaces.

### Record shape

`CustomTitleRecordPayload` grows an optional `titleSource: 'auto' |
'manual'` field:

```jsonc
{
  "type": "system",
  "subtype": "custom_title",
  "systemPayload": {
    "customTitle": "Debug login button on mobile",
    "titleSource": "auto",
  },
}
```

The field is optional, and absent-in-legacy records are treated as
`undefined`. `SessionPicker` dims rows only on a strict `=== 'auto'`
match — a pre-change user `/rename` title is never silently reclassified
as a model guess.

### Resume hydration

On resume, `ChatRecordingService` constructor calls
`sessionService.getSessionTitleInfo(sessionId)` to read **both** the
title and its source. Without hydrating the source, `finalize()`'s
re-append (which runs on every session lifecycle event) would rewrite
auto as manual on every resume cycle — silently stripping the dim
affordance.

### Atomic pair read

`extractLastJsonStringFields` returns `customTitle` and `titleSource`
from the **same matching line** in a single scan. Two separate
`readLastJsonStringFieldSync` calls could land on different records if
an older line has only the primary field, yielding a mismatched pair.
The extractor also requires a proper closing quote on the primary value,
so a crash-truncated trailing record can't win the latest-match race.

### Full-file scan cap

Phase-2 (when the tail-window fast path misses) streams the whole file
in 64KB chunks. Capped at `MAX_FULL_SCAN_BYTES = 64 MB` so a corrupt
multi-GB JSONL can't freeze the session picker on the main event loop.
The picker's latency envelope survives corruption.

### Symlink defense

Session reads open with `O_NOFOLLOW` (falls back to plain read-only on
Windows, where the constant is not exposed). Defense in depth so a
symlink planted in `~/.qwen/projects/<proj>/chats/` can't redirect a
metadata read to an unrelated file.

## Concurrency and Edge Cases

### Trigger guard order

`maybeTriggerAutoTitle` checks these conditions in this exact order — each
short-circuits the rest so the cheap ones run first:

1. `currentCustomTitle` set → skip. Never overwrite manual / prior auto.
2. Writer degraded → skip. A fail-stop recorder cannot persist a title, so
   do not spend more fast-model tokens.
3. Explicit title write pending → skip. User and remote intent wins even
   before its JSONL write settles.
4. `autoTitleController !== undefined` → skip. One attempt at a time.
5. `autoTitleAttempts >= 3` → skip. Cap bounds total waste.
6. `autoTitleDisabledByEnv()` → skip. `QWEN_DISABLE_AUTO_TITLE=1`
   explicit opt-out.
7. `!config.isInteractive()` → skip. Headless `qwen -p` / CI never spends
   fast-model tokens on a one-shot session.
8. `!config.getFastModel()` → skip. No fast-model → no-op.

### Why the cap is 3, not 1

The first assistant turn can be a pure tool-call with no user-visible
text (e.g. the model opens with a `grep`). `tryGenerateSessionTitle`
returns `{ok: false, reason: 'empty_history'}` in that case. Without a
retry window, an entire session's chance at a title would be burned on
turn 1 before the user said anything interesting. Cap of 3 covers the
common "first turn is noise" case while still bounding runaway retry on
a persistently failing fast model.

### Cross-process manual-rename race

Two CLI tabs on the same session file can diverge in memory. Tab A runs
`/rename foo` and writes `titleSource: manual`. Tab B's
`ChatRecordingService` has its own `currentCustomTitle = undefined` and
would naively overwrite with an auto title.

After the LLM call resolves, the IIFE re-reads the JSONL via
`sessionService.getSessionTitleInfo`. If the file shows
`source: 'manual'`, the IIFE bails AND syncs its in-memory state so
subsequent turns respect the rename too. Cost: one 64KB tail read per
successful generation; negligible.

### Abort propagation on `finalize()`

`autoTitleController` doubles as the in-flight flag. `finalize()` (run
on session switch and process shutdown) calls
`autoTitleController.abort()` before re-appending the title record. The
LLM socket is cancelled promptly; session switch doesn't wait on a slow
fast-model call. The IIFE's `finally` block clears
`autoTitleController` only if it's still the active one, so a finalize
mid-flight doesn't race a concurrent `recordAssistantTurn`.

### Explicit rename lands mid-flight

Every public `recordCustomTitle()` call increments a pending-explicit counter
and aborts the active background controller before it waits for JSONL. This
includes `/rename --auto`, so a model-generated explicit rename has the same
priority as a literal user title. The background task checks the counter,
abort signal, current title, and writer state both after generation and just
before persistence. Concurrent explicit renames are serialized by the writer;
their in-memory results commit in that same order. If an explicit title arrives
after a background title is already queued, normal writer ordering makes the
explicit title the final record.

While an explicit title write is pending, `finalize()` does not re-anchor the
previous cached title. Otherwise that stale record could be queued behind the
rename and become the JSONL tail after the new title successfully lands.
Title-anchor accounting follows logical writer queue order. Descendant records
queued behind any unresolved title write are counted, but threshold re-anchor
is deferred until the final cached title is known, so neither shutdown nor the
32KB anchor path can append a stale title behind a successful rename.

The cross-process re-read still protects manual titles written by another CLI
process. It is not a compare-and-swap; an external rename in the final check-to-
write window remains an accepted race.

### Branch title ordering

`/branch` finalizes and flushes the source recorder before copying it. It then
loads the provisional fork, computes a unique branch title, persists that title
through `SessionService`, and reloads the fork so its final tail and title cache
include the appended record. Only after those steps succeed does it switch the
core recorder and UI. A source flush failure, title failure, or final reload
failure deletes the incomplete fork and leaves the original session active.
`forkSession()` also removes its exclusive-created target if writing or closing
the new JSONL fails before the call can report success, so the caller cannot
leak a partial file during the window before it marks the fork as created.

### Daemon boundary

Live ACP title operations await `recordCustomTitle()` and therefore report a
truthful `persisted` or `success` result without a second `flush()` that could
attribute a later unrelated write failure to the rename. The daemon metadata
PATCH remains intentionally optimistic: it updates live state and publishes
its event immediately, while child persistence is best-effort and a false
result is debug-logged. Strongly consistent HTTP metadata updates are outside
this design.

## Configuration

### User-facing knobs

| Setting / env var           | Default | Effect                                                                                              |
| --------------------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `fastModel`                 | unset   | Required for auto-titling. Unset → no-op (no main-model fallback).                                  |
| `QWEN_DISABLE_AUTO_TITLE=1` | unset   | Opt out of the auto trigger without unsetting `fastModel`. `/rename --auto` still works on request. |

No `settings.json` toggle — the env var is the only user-visible
off-switch. Rationale: the feature is cosmetic and cheap; a settings
toggle would add a UI surface for something that can live as a one-time
env export for the few users who want to disable it.

### Why auto doesn't fall back to the main model

Auto-titling is triggered unconditionally after every assistant turn.
If a user without a fast model were silently charged main-model tokens
for every new session's title, the cost delta is invisible until the
monthly bill arrives. Failing quietly (no-op, no title, no cost) is the
safer default. `/rename --auto` surfaces `no_fast_model` as an
actionable error so the user can set one if they want to.

## Observability

`createDebugLogger('SESSION_TITLE')` emits `debugLogger.warn` from the
generator's catch block. Failures are fully transparent to the user —
auto-title is an auxiliary feature and never throws into the UI.

Developers can grep for the `[SESSION_TITLE]` tag in the debug log
(`~/.qwen/debug/<sessionId>.txt`; `latest.txt` symlinks to the current
session). A working end-to-end call produces no log output; a failing
one gets one WARN line with the underlying error message.

## Security Hardening

The title value is rendered verbatim in the terminal (session picker)
AND persisted in a user-readable JSONL file. Both surfaces are attack
reachable if a compromised or prompt-injected fast model returns
hostile text.

| Concern                                     | Guard                                                                                                                         |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| ANSI / OSC-8 / CSI injection                | `stripTerminalControlSequences` before both JSONL write and picker render.                                                    |
| Clickable-link smuggle via OSC-8            | Same — OSC sequences stripped as whole units, not just the ESC byte.                                                          |
| Invalid UTF-16 surrogates                   | Scrubbed in `flattenToTail` (LLM input) and `sanitizeTitle` (LLM output after max-length trim).                               |
| Subtype-line spoof via user message content | `lineContains: '"subtype":"custom_title"'` — user text that happens to contain the literal phrase can't shadow a real record. |
| Symlink redirect on session reads           | `O_NOFOLLOW` (no-op on Windows where the constant is missing).                                                                |
| Truncated trailing JSONL record             | `extractLastJsonStringFields` requires a closing quote before a record wins the latest-match race.                            |
| Pathological file size freezing the picker  | `MAX_FULL_SCAN_BYTES = 64 MB` cap on Phase-2 full-file scan.                                                                  |
| Paired CJK bracket decorators (`【Draft】`) | Stripped as a unit so a lone closing bracket doesn't dangle.                                                                  |

## Out of Scope

| Item                                        | Why not                                                                                                                                     |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Auto-regenerate when the title goes stale   | `/rename --auto` is the explicit user-triggered path. Silent mid-session title swaps would confuse users scrolling back through the picker. |
| WebUI / VSCode dim-styling parity           | Those surfaces read `customTitle` already and will show auto titles as if manual. A follow-up can wire the `titleSource` through.           |
| Settings-dialog toggle for auto generation  | Env var is the single knob. Full settings UI is easy to add later if user demand surfaces.                                                  |
| i18n locale catalog entries for new strings | Consistent with existing `/rename` strings, which fall through to English. A repo-wide i18n pass is out of scope.                           |
| Migration to re-classify legacy records     | Back-compat by design: absent `titleSource` is treated as manual. Rewriting old records would risk losing user intent.                      |
| Non-interactive auto-titling                | `qwen -p` / CI scripts throw the session away; fast-model tokens for a title no one will ever resume is pure waste.                         |

---

### Design Slash Command Compare (`docs/design/slash-command/compare.md`)

# Qwen Code Command 模块重构方案

## 1. 目标定义

本方案以以下原则为唯一前提：

- **代码架构可以不照搬 Claude Code**
- **但命令系统的核心功能、使用体验、交互体验必须 95% 对齐 Claude Code**

这里的“对齐”指用户可直接感知的能力，包括：

1. 命令来源覆盖
2. 命令帮助与发现性
3. 命令补全与 mid-input slash command 体验
4. ACP / non-interactive 可用性
5. prompt command / skill 的模型调用能力

本次重构不是补几个字段，也不是把现有 `SlashCommand` 小修小补，而是把 command 模块从“interactive UI 附属能力”升级为“跨 interactive / ACP / non-interactive / model 的统一命令平台”。

---

## 2. 重写后的结论

Qwen 现有 command 系统的问题，不是完全没有能力，而是：

1. 只在 interactive 主路径上较完整
2. 类型模型太薄，无法承载 Claude 级别的产品面
3. ACP / non-interactive 依赖白名单，扩展性极差
4. command 来源虽然存在，但没有形成对用户可见的统一心智
5. prompt command 与模型 skill 暴露体系割裂

因此新的方案必须同时解决四件事：

1. **补齐 Claude Code 的能力面**
2. **保留 Qwen 统一 outcome 模型的工程优势**
3. **建立统一 registry / resolver / executor / adapter 架构**
4. **让帮助、补全、ACP available commands、文档共用同一套元数据**

---

## 3. 重构原则

### 3.1 功能对齐优先于实现对齐

允许不同：

- 内部类名
- 模块拆分方式
- 执行器实现
- effect / outcome 结构

不允许不同：

- 命令来源覆盖明显缩水
- 命令帮助和补全体验明显缩水
- ACP / non-interactive 可用性明显缩水
- prompt command 与模型能力融合明显缩水

如果出现取舍，优先级应为：

1. 用户体验对齐
2. 命令能力覆盖对齐
3. 模式一致性对齐
4. 内部实现简洁

### 3.2 保留 Qwen 的统一 outcome 模型

不建议机械复制 Claude 的执行实现。

Qwen 当前统一结果模型仍然值得保留，因为它天然适合：

- UI 接管
- 审批/确认
- tool 调度
- prompt 提交
- 跨模式适配

但它必须被升级为能够承载 Claude 级别的 command 能力，而不是继续作为简化版 UI 命令框架存在。

### 3.3 类型、来源、模式、可见性必须彻底解耦

新的 command 模型至少要把以下维度拆开：

1. **类型**：命令怎么执行
2. **来源**：命令从哪里来
3. **模式能力**：在哪些运行环境可用
4. **可见性**：对用户可见还是对模型可见

---

## 4. 需要对齐的 Claude Code 能力面

### 4.1 命令类型

Qwen 需要显式支持三类命令：

1. `prompt`
2. `local`
3. `local-jsx`

### 4.2 命令来源

Qwen 的 command schema 从第一阶段开始就必须覆盖以下来源：

1. built-in commands
2. bundled skills
3. skill dir commands
4. workflow commands
5. plugin commands
6. plugin skills
7. dynamic skills
8. mcp prompts
9. mcp skills

这里不能再退回到“先只支持当前已有那几类”。

### 4.3 命令元数据

至少补齐以下字段：

1. `argumentHint`
2. `whenToUse`
3. `examples`
4. `sourceLabel`
5. `userFacingName`
6. `alias`
7. `immediate`
8. `isSensitive`
9. `userInvocable`
10. `modelInvocable`
11. `supportedModes`
12. `requiresUi`

### 4.4 体验能力

至少补齐以下体验：

1. alias 命中补全
2. source badge
3. 参数提示
4. recently used 排序
5. mid-input slash command 检测与补全
6. 命令目录式 Help
7. ACP available commands 的完整表达

---

## 5. 新 command 模型

## 5.1 核心结构

建议引入统一 `CommandDescriptor`，作为所有命令的注册格式。

它至少包含四部分：

1. `identity`
2. `metadata`
3. `capabilities`
4. `handler`

### `identity`

- `id`
- `name`
- `altNames`
- `canonicalPath`

### `metadata`

- `description`
- `argumentHint`
- `whenToUse`
- `examples`
- `group`
- `source`
- `sourceLabel`
- `userFacingName`
- `hidden`

### `capabilities`

- `type`: `prompt | local | local-jsx`
- `supportedModes`: `interactive | acp | non_interactive`
- `requiresUi`
- `supportsDialog`
- `supportsStreaming`
- `supportsToolInvocation`
- `supportsConfirmation`
- `remoteSafe`
- `readOnly`
- `immediate`
- `isSensitive`
- `userInvocable`
- `modelInvocable`

### `handler`

- `resolveArgs()`
- `execute()`
- `completion()`
- `fallback()`

---

## 5.2 三种命令类型的职责

### `prompt`

用于：

- skills
- file commands
- workflow prompt commands
- plugin skills
- mcp prompt / skill

特点：

- 产生 prompt / skill 资产
- 默认支持 interactive / ACP / non-interactive
- 可以被用户调用，也可以被模型调用

### `local`

用于：

- 查询类命令
- 配置类命令
- headless 可执行的状态类命令
- 大多数 built-in commands 的核心执行入口

特点：

- 不依赖 UI
- 应成为 ACP / non-interactive 的主承载类型

### `local-jsx`

用于：

- picker
- 面板
- wizard
- interactive UI shell

特点：

- 只处理 interactive UI
- 不能再作为唯一执行入口
- 必须提供 fallback 或对应 local 子命令

---

## 6. 命令来源模型

## 6.1 外部来源模型

这是给用户看的来源模型，必须和 Claude Code 的心智尽量一致：

- `builtin-command`
- `bundled-skill`
- `skill-dir-command`
- `workflow-command`
- `plugin-command`
- `plugin-skill`
- `dynamic-skill`
- `builtin-plugin-skill`
- `mcp-prompt`
- `mcp-skill`

这组字段将直接用于：

- Help 分组
- Completion source badge
- ACP available commands
- 文档导出

## 6.2 内部归一化模型

为了不被外部命名绑死，内部再补一层实现字段：

- `providerType`
- `artifactType`
- `activationMode`
- `builtinProvided`
- `originPath`
- `namespace`

这样可以做到：

- 外部体验按 Claude 对齐
- 内部实现仍保持 Qwen 可维护性

## 6.3 冲突策略

统一按稳定 `id` 管理，展示名和输入名分离：

1. `id`：稳定唯一标识
2. `name`：输入主名
3. `userFacingName`：帮助/补全展示名

冲突优先级建议：

1. built-in
2. bundled / skill-dir / workflow
3. plugin / builtin-plugin
4. dynamic
5. mcp 独立 namespace

---

## 7. 统一执行架构

## 7.1 `CommandRegistry`

职责：

1. 聚合所有 loader/provider
2. 建立多维索引
3. 输出帮助、补全、ACP、文档视图
4. 提供用户可见命令和模型可见命令的独立视图

必须支持的 provider：

1. `BuiltinCommandLoader`
2. `BundledSkillLoader`
3. `FileCommandLoader`
4. `McpPromptLoader`
5. `WorkflowCommandLoader`
6. `PluginCommandLoader`
7. `PluginSkillLoader`
8. `DynamicSkillProvider`
9. `BuiltinPluginSkillLoader`

即便部分 provider 首期未完全落地，schema 和 API 也必须先支持。

## 7.2 `CommandResolver`

职责：

1. 解析 slash command
2. 解析 alias
3. 解析 subcommand path
4. 识别 mid-input slash token
5. 输出 canonical resolved command

## 7.3 `CommandExecutor`

职责：

1. 做 capability 检查
2. 执行 `prompt | local | local-jsx`
3. 统一产出 outcome
4. 处理 fallback / unsupported

## 7.4 `ModeAdapter`

必须拆出三种 adapter：

1. `InteractiveModeAdapter`
2. `AcpModeAdapter`
3. `NonInteractiveModeAdapter`

这样三种模式才能共用同一套 command registry 和 executor，而不是各自硬编码。

---

## 8. UI 命令重构原则：核心命令与交互壳分离

这是 ACP 和 non-interactive 真正可用的关键。

凡是当前本质为“打开 dialog”的命令，都必须改造成：

1. 一个 interactive shell
2. 一组 local 子命令

### 第一批必须拆分的命令

1. `/model`
2. `/permissions`
3. `/mcp`
4. `/resume`
5. `/hooks`
6. `/extensions`
7. `/agents`
8. `/approval-mode`

### 目标形态示例

#### `/model`

- `/model`
- `/model show`
- `/model list`
- `/model set <id>`

#### `/permissions`

- `/permissions`
- `/permissions show`
- `/permissions set <mode>`
- `/permissions allow <tool>`
- `/permissions deny <tool>`

#### `/mcp`

- `/mcp`
- `/mcp list`
- `/mcp show <server>`
- `/mcp enable <server>`
- `/mcp disable <server>`

---

## 9. Prompt Command / Skill 统一设计

这是重构里的 P0，不是后补能力。

## 9.1 目标

建立统一的 **Model-Invocable Prompt Command Registry**，把以下资产合并为一个模型可调用视图：

1. bundled skills
2. file commands
3. workflow prompt commands
4. plugin skills
5. mcp prompts / mcp skills

## 9.2 关键字段

必须新增：

1. `userInvocable`
2. `modelInvocable`
3. `allowedTools`
4. `whenToUse`
5. `argSchema` 或最小参数描述
6. `contextMode: inline | fork`
7. `agent`
8. `effort`

## 9.3 与 `SkillTool` 的关系

重构后不应再由 `SkillTool` 只消费狭义 skills。

应改成：

1. `CommandRegistry.getModelInvocablePromptCommands()` 产出统一视图
2. `SkillTool` 或未来统一 command tool 消费该视图
3. 用户 slash command 与模型 skill invocation 共用同一套 prompt-command 资产池

这样 Qwen 才能在体验上接近 Claude 对 `/review`、`/commit`、`/openspec-apply` 这类能力的处理方式。

---

## 10. Help / Completion / Discoverability 重做

## 10.1 Completion

补全项至少要展示：

1. `label`
2. `description`
3. `argumentHint`
4. `sourceBadge`
5. `modeBadges`
6. `aliasHit`
7. `recentlyUsedScore`

排序至少考虑：

1. 精确命中
2. alias 命中
3. 最近使用
4. prefix 命中
5. fuzzy 命中

## 10.2 Mid-input slash command

必须补齐：

1. 光标附近 slash token 检测
2. ghost text 提示
3. Tab 完成
4. 有效命令 token 高亮

第一阶段先对齐输入体验；是否引入更强的“内嵌命令执行语义”可在后续迭代。

## 10.3 Help

Help 不再是平铺列表，而是完整命令目录。

至少分组为：

1. Built-in Commands
2. Bundled Skills
3. Skill Dir Commands
4. Workflow Commands
5. Plugin Commands
6. Plugin Skills
7. Dynamic Skills
8. Builtin Plugin Skills
9. MCP Commands / MCP Skills

每条命令至少展示：

1. 名称
2. 参数提示
3. 描述
4. 来源
5. 支持模式
6. 是否模型可调用
7. 子命令摘要

---

## 11. ACP / Non-Interactive 重构

## 11.1 彻底废弃白名单思路

旧方案：

- built-in allowlist
- FILE / SKILL 特判
- 其它结果类型 unsupported

新方案：

- 每个命令自己声明 capability
- registry 负责过滤
- adapter 负责执行和 fallback

## 11.2 outcome 支持目标

### interactive

- `submit_prompt`
- `message`
- `stream_messages`
- `tool`
- `dialog`
- `load_history`
- `confirm_action`
- `confirm_shell_commands`

### acp

- `submit_prompt`
- `message`
- `stream_messages`
- `tool`
- `confirm_action`
- `confirm_shell_commands`
- `dialog fallback`

### non_interactive

- `submit_prompt`
- `message`
- `stream_messages`
- `tool`
- `confirm_action`
- `confirm_shell_commands`
- `dialog fallback / structured failure`

## 11.3 ACP available commands 输出

必须至少包含：

1. `name`
2. `description`
3. `argumentHint`
4. `source`
5. `examples`
6. `supportedModes`
7. `interactiveOnly`
8. `subcommands`
9. `modelInvocable`

---

## 12. 文档、帮助、补全共用同一份元数据

重构后以下内容必须由同一个 registry 视图导出：

1. Help
2. Completion
3. ACP available commands
4. 文档导出

这是为了解决当前“实现、帮助、文档三套命令面不一致”的问题。

---

## 13. 实施分期

## Phase 1：底座重建

交付：

1. 新 `CommandDescriptor`
2. 完整来源 schema
3. capability 模型
4. `userInvocable / modelInvocable`
5. `CommandRegistry`
6. `CommandResolver`
7. `CommandExecutor`
8. 三种 `ModeAdapter`
9. `getModelInvocablePromptCommands()`

## Phase 2：核心命令迁移

交付：

1. `/model`
2. `/permissions`
3. `/mcp`
4. `/resume`
5. `/hooks`
6. `/extensions`
7. `/agents`
8. `/approval-mode`

这些命令都必须完成“interactive shell + local 子命令”重构。

## Phase 3：模型能力打通

交付：

1. `SkillTool` 接入统一 registry 视图
2. file command / bundled skill / mcp prompt / plugin skill 进入统一 model-invocable 集合
3. prompt command 与 skill 资产彻底统一

## Phase 4：体验层对齐 Claude

交付：

1. recently used 排序
2. source badge
3. argument hint
4. mode badge
5. 完整 help 目录
6. mid-input slash command 体验
7. 文档自动导出或校验

---

## 14. 验收标准

完成后至少满足：

1. 帮助、补全、ACP、文档都能表达完整来源模型
2. 除纯 UI 壳命令外，大多数 built-in command 可在 ACP / non-interactive 使用
3. prompt command 与模型 skill 调用使用同一套资产池
4. 命令体验在帮助、补全、来源表达、参数提示、mid-input 体验上达到 Claude Code 95% 水平
5. 不再依赖 built-in allowlist 维持 ACP / non-interactive 命令能力

---

## 15. 最终判断

这次重构的本质不是“给现有 SlashCommand 多加几个字段”，而是：

- **用 Qwen 的内部架构风格，交付一个在外部体验上 95% 对齐 Claude Code 的 command 平台**

如果必须二选一：

- 内部实现更像 Claude
- 外部体验更像 Claude

本方案明确选择后者。

---

### Design Slash Command Phase1 Technical Design (`docs/design/slash-command/phase1-technical-design.md`)

# Phase 1 技术设计文档：基础设施重建

## 1. 设计目标与约束

### 1.1 目标

- 建立统一的命令元数据模型，覆盖来源（source）、执行类型（commandType）、模式能力（supportedModes）、可见性（userInvocable / modelInvocable）四个维度
- 用 capability-based 过滤替换 non-interactive/acp 中的硬编码白名单
- 为 Phase 2/3 的能力扩展提供稳定的底层接口

### 1.2 硬性约束

- **零行为变化**：non-interactive 和 acp 模式下现有可用命令集保持不变（例外：修复 MCP_PROMPT 被错误拦截，属于 bug fix）
- **向后兼容**：`SlashCommand` 接口的新增字段全部为可选或有合理默认值，现有命令代码无需立即修改
- **不新增执行器**：不创建 ModeAdapter / CommandExecutor 等新执行架构，只扩展现有 CommandService 和过滤逻辑
- **不改变现有命令能力**：不为任何命令新增 local 子命令，不修改任何命令的 action 实现

---

## 2. 新增类型定义

### 2.1 文件位置

所有新增类型定义在 `packages/cli/src/ui/commands/types.ts`，与现有 `SlashCommand` 接口共文件。

### 2.2 `ExecutionMode`

```typescript
/**
 * 运行模式枚举。
 * - interactive：React/Ink UI 模式（终端交互）
 * - non_interactive：无交互 CLI 模式（文本/JSON 输出）
 * - acp：ACP/Zed 集成模式
 */
export type ExecutionMode = 'interactive' | 'non_interactive' | 'acp';
```

### 2.3 `CommandSource`

```typescript
/**
 * 命令来源枚举，用于 Help 分组、补全 badge、ACP available commands。
 *
 * 与 CommandKind 的区别：
 * - CommandKind 是内部加载器分类（4 种），影响加载逻辑
 * - CommandSource 是面向用户的来源分类（9 种），影响展示和心智模型
 *
 * 两者可能重叠，但职责不同，不合并。
 */
export type CommandSource =
  | 'builtin-command' // 内置命令（BuiltinCommandLoader）
  | 'bundled-skill' // 随包分发的 skill（BundledSkillLoader）
  | 'skill-dir-command' // 用户/项目 .qwen/commands/ 下的文件命令（FileCommandLoader，非插件）
  | 'plugin-command' // 插件提供的命令（FileCommandLoader，extensionName 不为空）
  | 'mcp-prompt'; // MCP server 提供的 prompt（McpPromptLoader）
// 以下来源预留，Phase 1 不实现对应 Loader，但 schema 先定义：
// | 'workflow-command'
// | 'plugin-skill'
// | 'dynamic-skill'
// | 'builtin-plugin-skill'
// | 'mcp-skill'
```

### 2.4 `CommandType`

```typescript
/**
 * 命令执行类型，描述命令"怎么执行"。
 *
 * - prompt：产生 submit_prompt，将内容提交给模型。适用于 skill、file command、MCP prompt。
 *   默认 supportedModes 为所有模式，默认 modelInvocable 为 true。
 *
 * - local：在本地执行逻辑，不依赖 React/Ink UI。可返回 message、stream_messages、
 *   submit_prompt、tool 等类型。适用于查询类、配置类、状态类 built-in 命令。
 *   默认 supportedModes 为 ['interactive']，需显式声明 supportedModes 才能开放给其他模式。
 *   这与 Claude Code 的 supportsNonInteractive: true 语义一致——非交互支持需要显式声明，而非自动推断。
 *
 * - local-jsx：依赖 React/Ink UI 的命令（打开 dialog、渲染 JSX 组件等）。
 *   默认 supportedModes 仅为 ['interactive']。
 */
export type CommandType = 'prompt' | 'local' | 'local-jsx';
```

### 2.5 扩展 `SlashCommand` 接口

在现有接口上追加新字段，**全部为可选**以保证向后兼容：

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## 3. 各 Loader 的字段填充规范

### 3.1 填充原则

- `source` 和 `sourceLabel` 由 Loader 在构建 `SlashCommand` 时填充，命令自身不声明
- `commandType`：Loader 填充默认值；built-in 命令由命令文件自身声明
- `supportedModes`：通过 `getEffectiveSupportedModes()` 推断，不需要显式填充（除非需要覆盖默认值）
- `modelInvocable`：Loader 填充，built-in 命令始终为 `false`，prompt 类型命令为 `true`

### 3.2 `BuiltinCommandLoader`

```typescript
// 不填充 source/sourceLabel/commandType — 由各命令文件自声明
// 因为 built-in 命令的 commandType 是 local 或 local-jsx，需要逐个标注

// 注入 source 和 sourceLabel：
for (const cmd of rawCommands) {
  enrichedCommands.push({
    ...cmd,
    source: 'builtin-command',
    sourceLabel: 'Built-in',
    userInvocable: cmd.userInvocable ?? true,
    modelInvocable: false, // built-in 命令不允许模型调用
  });
}
```

### 3.3 `BundledSkillLoader`

```typescript
return skills.map((skill) => ({
  name: skill.name,
  description: skill.description,
  kind: CommandKind.SKILL,
  source: 'bundled-skill' as CommandSource,
  sourceLabel: 'Skill',
  commandType: 'prompt' as CommandType,
  userInvocable: true,
  modelInvocable: true,
  action: async (...) => { ... },
}));
```

### 3.4 `FileCommandLoader`

```typescript
// 在 createSlashCommandFromDefinition 中：
return {
  name: baseCommandName,
  description,
  kind: CommandKind.FILE,
  extensionName,
  // source 根据 extensionName 决定：
  source: extensionName ? 'plugin-command' : 'skill-dir-command',
  sourceLabel: extensionName ? `Plugin: ${extensionName}` : 'Custom',
  commandType: 'prompt',
  userInvocable: true,
  modelInvocable: !extensionName, // 插件命令暂不允许模型调用，用户/项目命令允许
  action: async (...) => { ... },
};
```

> **注**：插件命令（plugin-command）暂不标记为 `modelInvocable`，避免安全隐患。后续 Phase 可以按需开放，由用户通过配置控制。

### 3.5 `McpPromptLoader`

```typescript
const newPromptCommand: SlashCommand = {
  name: commandName,
  description: prompt.description || `Invoke prompt ${prompt.name}`,
  kind: CommandKind.MCP_PROMPT,
  source: 'mcp-prompt',
  sourceLabel: `MCP: ${serverName}`,
  commandType: 'prompt',
  userInvocable: true,
  modelInvocable: true,
  // ... 其余现有字段
};
```

---

## 4. Built-in 命令的 `commandType` 声明规范

### 4.1 分类标准

| commandType | 判断标准                                                                                                                                                                   |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `local`     | action 只使用 `ui.addItem`（文本类型）、返回 `message` / `stream_messages` / `submit_prompt` / `tool`，不依赖 React 组件渲染                                               |
| `local-jsx` | action 返回 `dialog`，或 action 中调用 `ui.addItem` 时传入含 JSX 的复杂类型（如 `HistoryItemHelp`、`HistoryItemStats`），或依赖 `confirm_action` / `load_history` / `quit` |

> **注意**：`ui.addItem(message/error/info 类型)` 是 `local`；`ui.addItem(help/stats/tools/about 等复杂 UI 类型)` 是 `local-jsx`。

### 4.2 Built-in 命令分类表

**`local` 类**（声明 `commandType: 'local'`，`supportedModes` 推断为 all modes）：

| 命令文件             | 命令名     | 说明                                                    |
| -------------------- | ---------- | ------------------------------------------------------- |
| `btwCommand.ts`      | `btw`      | 返回 `submit_prompt` 或 `stream_messages`               |
| `bugCommand.ts`      | `bug`      | 返回 `submit_prompt` 或 `stream_messages`               |
| `compressCommand.ts` | `compress` | 已有 executionMode 适配，返回 `message`/`submit_prompt` |
| `contextCommand.ts`  | `context`  | 返回 `message`（含 UI 渲染但文本可替代）                |
| `exportCommand.ts`   | `export`   | 文件 I/O，返回 `message`                                |
| `initCommand.ts`     | `init`     | 返回 `submit_prompt`/`message`/`confirm_action`         |
| `memoryCommand.ts`   | `memory`   | 子命令返回 `message`（文件 I/O）                        |
| `planCommand.ts`     | `plan`     | 返回 `submit_prompt`                                    |
| `summaryCommand.ts`  | `summary`  | 已有 executionMode 适配，返回 `submit_prompt`/`message` |
| `insightCommand.ts`  | `insight`  | 返回 `stream_messages`                                  |

> **注意**：`contextCommand` 和 `insightCommand` 虽然当前返回 `addItem` 调用，但其本质是文本内容，属于 `local`。

**`local-jsx` 类**（声明 `commandType: 'local-jsx'`，`supportedModes` 推断为 `['interactive']`）：

| 命令文件                  | 命令名           | 不能 headless 的原因                       |
| ------------------------- | ---------------- | ------------------------------------------ |
| `aboutCommand.ts`         | `about`          | `addItem(HistoryItemAbout)` — 复杂 UI 组件 |
| `agentsCommand.ts`        | `agents`         | `dialog: subagent_create/subagent_list`    |
| `approvalModeCommand.ts`  | `approval-mode`  | `dialog: approval-mode`                    |
| `arenaCommand.ts`         | `arena`          | `dialog: arena_*`                          |
| `authCommand.ts`          | `auth`           | `dialog: auth`                             |
| `clearCommand.ts`         | `clear`          | `ui.clear()` 直接操作终端                  |
| `copyCommand.ts`          | `copy`           | 剪贴板操作，无 headless 路径               |
| `directoryCommand.tsx`    | `directory`      | JSX 组件                                   |
| `docsCommand.ts`          | `docs`           | 打开浏览器                                 |
| `editorCommand.ts`        | `editor`         | `dialog: editor`                           |
| `extensionsCommand.ts`    | `extensions`     | `dialog: extensions_manage`                |
| `helpCommand.ts`          | `help`           | `addItem(HistoryItemHelp)` — 复杂 Help UI  |
| `hooksCommand.ts`         | `hooks`          | `dialog: hooks`                            |
| `ideCommand.ts`           | `ide`            | IDE 进程检测与交互                         |
| `languageCommand.ts`      | `language`       | `dialog` + `reloadCommands`                |
| `mcpCommand.ts`           | `mcp`            | `dialog: mcp`                              |
| `modelCommand.ts`         | `model`          | `dialog: model/fast-model`                 |
| `permissionsCommand.ts`   | `permissions`    | `dialog: permissions`                      |
| `quitCommand.ts`          | `quit`           | `quit` result 类型                         |
| `restoreCommand.ts`       | `restore`        | `load_history` result 类型                 |
| `resumeCommand.ts`        | `resume`         | `dialog: resume`                           |
| `settingsCommand.ts`      | `settings`       | `dialog: settings`                         |
| `setupGithubCommand.ts`   | `setup-github`   | `confirm_shell_commands` + 交互式操作      |
| `skillsCommand.ts`        | `skills`         | `addItem(HistoryItemSkillsList)` — 复杂 UI |
| `statsCommand.ts`         | `stats`          | `addItem(HistoryItemStats)` — 复杂 UI      |
| `statuslineCommand.ts`    | `statusline`     | UI 状态配置                                |
| `terminalSetupCommand.ts` | `terminal-setup` | 终端配置向导                               |
| `themeCommand.ts`         | `theme`          | `dialog: theme`                            |
| `toolsCommand.ts`         | `tools`          | `addItem(HistoryItemTools)` — 复杂 UI      |
| `trustCommand.ts`         | `trust`          | `dialog: trust`                            |
| `vimCommand.ts`           | `vim`            | `toggleVimEnabled()` — UI 状态             |

---

## 5. `getEffectiveSupportedModes` 推断规则

此函数是 Phase 1 的核心逻辑，替代原有白名单，将被 `filterCommandsForMode` 调用。

```typescript
/**
 * 获取命令的实际支持模式列表。
 *
 * 推断优先级（从高到低）：
 * 1. 命令显式声明的 supportedModes（最高优先级）
 * 2. 基于 commandType 的推断
 * 3. 基于 CommandKind 的兜底（向后兼容）
 */
export function getEffectiveSupportedModes(cmd: SlashCommand): ExecutionMode[] {
  // 优先级 1：显式声明
  if (cmd.supportedModes !== undefined) {
    return cmd.supportedModes;
  }

  // 优先级 2：基于 commandType 推断
  if (cmd.commandType !== undefined) {
    switch (cmd.commandType) {
      case 'prompt':
        // prompt 类型无 UI 依赖，天然全模式可用
        return ['interactive', 'non_interactive', 'acp'];
      case 'local':
        // local 类型保守默认：仅 interactive。
        // 需要非交互支持的命令须显式声明 supportedModes（对应 Claude Code 的 supportsNonInteractive: true）。
        // Phase 2 中逐个验证并解锁，防止未适配的命令意外暴露给 headless 调用者。
        return ['interactive'];
      case 'local-jsx':
        return ['interactive'];
    }
  }

  // 优先级 3：兜底（基于 CommandKind，向后兼容旧代码）
  switch (cmd.kind) {
    case CommandKind.BUILT_IN:
      // built-in 命令未声明 commandType 时保守默认（interactive only）
      // 这个分支在 Phase 1 完成后应不再被命中（所有 built-in 都有 commandType）
      return ['interactive'];
    case CommandKind.FILE:
    case CommandKind.SKILL:
    case CommandKind.MCP_PROMPT:
      // 这三类命令的 action 天然无 UI 依赖，历史行为也是全模式可用
      return ['interactive', 'non_interactive', 'acp'];
    default:
      return ['interactive'];
  }
}
```

```typescript
/**
 * 根据 supportedModes 过滤适合当前模式的命令。
 * 替代原 filterCommandsForNonInteractive 函数。
 */
export function filterCommandsForMode(
  commands: readonly SlashCommand[],
  mode: ExecutionMode,
): SlashCommand[] {
  return commands.filter((cmd) =>
    getEffectiveSupportedModes(cmd).includes(mode),
  );
}
```

---

## 6. `CommandService` 接口扩展

在 `packages/cli/src/services/CommandService.ts` 中新增两个方法：

```typescript
export class CommandService {
  // ── 现有方法（保持不变）────────────────────────────────────────────────
  getCommands(): readonly SlashCommand[] {
    return this.commands;
  }

  // ── Phase 1 新增方法 ──────────────────────────────────────────────────

  /**
   * 返回在指定执行模式下可用的命令列表。
   * 替代原有白名单 + filterCommandsForNonInteractive 的组合。
   *
   * @param mode 目标运行模式
   * @returns 适合该模式的命令列表（不含 hidden 命令）
   */
  getCommandsForMode(mode: ExecutionMode): readonly SlashCommand[] {
    return this.commands.filter((cmd) => {
      if (cmd.hidden) return false;
      return getEffectiveSupportedModes(cmd).includes(mode);
    });
  }

  /**
   * 返回所有 modelInvocable 为 true 的命令。
   * Phase 2 中 SkillTool 将消费此方法；Phase 1 仅提供接口。
   *
   * @returns 模型可调用的命令列表
   */
  getModelInvocableCommands(): readonly SlashCommand[] {
    return this.commands.filter(
      (cmd) => !cmd.hidden && cmd.modelInvocable === true,
    );
  }
}
```

> **注意**：`getEffectiveSupportedModes` 和 `filterCommandsForMode` 应作为 `CommandService` 内部使用的工具函数，或提取到独立的 `packages/cli/src/services/commandUtils.ts` 文件并导出，以便测试和复用。

---

## 7. `nonInteractiveCliCommands.ts` 重构

### 7.1 删除内容

```typescript
// ❌ 删除
export const ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE = [
  'init', 'summary', 'compress', 'btw', 'bug', 'context',
] as const;

// ❌ 删除
function filterCommandsForNonInteractive(
  commands: readonly SlashCommand[],
  allowedBuiltinCommandNames: Set<string>,
): SlashCommand[] { ... }
```

### 7.2 新增内容

```typescript
// ✅ 新增（或从 commandUtils 导入）
import { filterCommandsForMode } from '../services/commandUtils.js';
```

### 7.3 `handleSlashCommand` 函数签名变更

```typescript
// ❌ 旧签名
export const handleSlashCommand = async (
  rawQuery: string,
  abortController: AbortController,
  config: Config,
  settings: LoadedSettings,
  allowedBuiltinCommandNames: string[] = [...ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE],
): Promise<NonInteractiveSlashCommandResult>

// ✅ 新签名（移除 allowedBuiltinCommandNames）
export const handleSlashCommand = async (
  rawQuery: string,
  abortController: AbortController,
  config: Config,
  settings: LoadedSettings,
): Promise<NonInteractiveSlashCommandResult>
```

### 7.4 内部实现变更

```typescript
// 旧：
const filteredCommands = filterCommandsForNonInteractive(
  allCommands,
  allowedBuiltinSet,
);

// 新：
const executionMode = isAcpMode ? 'acp' : 'non_interactive';
const filteredCommands = filterCommandsForMode(allCommands, executionMode);
```

### 7.5 `getAvailableCommands` 函数签名变更

```typescript
// ❌ 旧签名
export const getAvailableCommands = async (
  config: Config,
  abortSignal: AbortSignal,
  allowedBuiltinCommandNames: string[] = [...ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE],
): Promise<SlashCommand[]>

// ✅ 新签名
export const getAvailableCommands = async (
  config: Config,
  abortSignal: AbortSignal,
  mode: ExecutionMode = 'acp',
): Promise<SlashCommand[]>
```

> 新增 `mode` 参数替代原来的白名单参数，ACP Session 调用时可明确指定 `'acp'`，non-interactive 调用时指定 `'non_interactive'`。

---

## 8. `Session.ts`（ACP）调用变更

```typescript
// ❌ 旧调用
const slashCommandResult = await handleSlashCommand(
  inputText,
  abortController,
  this.config,
  this.settings,
  // 不传，使用默认白名单
);

// ✅ 新调用（无变化，移除了不再存在的默认参数）
const slashCommandResult = await handleSlashCommand(
  inputText,
  abortController,
  this.config,
  this.settings,
);

// ─────────────────────────────────────────

// ❌ 旧调用
const slashCommands = await getAvailableCommands(
  this.config,
  abortController.signal,
);

// ✅ 新调用（明确指定 mode）
const slashCommands = await getAvailableCommands(
  this.config,
  abortController.signal,
  'acp',
);
```

---

## 9. 文件变更总览

### 9.1 修改的文件

| 文件                                                                    | 修改内容                                                                                         |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `packages/cli/src/ui/commands/types.ts`                                 | 新增 `ExecutionMode`、`CommandSource`、`CommandType` 类型；扩展 `SlashCommand` 接口              |
| `packages/cli/src/services/CommandService.ts`                           | 新增 `getCommandsForMode()`、`getModelInvocableCommands()` 方法                                  |
| `packages/cli/src/nonInteractiveCliCommands.ts`                         | 删除白名单常量和旧过滤函数；更新两个导出函数的签名；引入 `filterCommandsForMode`                 |
| `packages/cli/src/acp-integration/session/Session.ts`                   | 更新 `handleSlashCommand` 和 `getAvailableCommands` 调用                                         |
| `packages/cli/src/services/BuiltinCommandLoader.ts`                     | 在构建命令时注入 `source: 'builtin-command'`、`sourceLabel: 'Built-in'`、`modelInvocable: false` |
| `packages/cli/src/services/BundledSkillLoader.ts`                       | 注入 `source: 'bundled-skill'`、`commandType: 'prompt'`、`modelInvocable: true`                  |
| `packages/cli/src/services/FileCommandLoader.ts` / `command-factory.ts` | 注入 `source`、`commandType: 'prompt'`、`modelInvocable`（根据 extensionName）                   |
| `packages/cli/src/services/McpPromptLoader.ts`                          | 注入 `source: 'mcp-prompt'`、`commandType: 'prompt'`、`modelInvocable: true`                     |
| **各 built-in 命令文件（10 个 local + 27 个 local-jsx）**               | 声明 `commandType: 'local'` 或 `commandType: 'local-jsx'`                                        |

### 9.2 新增的文件

| 文件                                        | 内容                                                                       |
| ------------------------------------------- | -------------------------------------------------------------------------- |
| `packages/cli/src/services/commandUtils.ts` | `getEffectiveSupportedModes()`、`filterCommandsForMode()` 工具函数及其导出 |

### 9.3 不变的文件

- `packages/cli/src/ui/commands/commands.ts`（`parseSlashCommand` 无需修改）
- `packages/cli/src/ui/hooks/slashCommandProcessor.ts`（interactive 路径无需修改）
- `packages/cli/src/ui/noninteractive/nonInteractiveUi.ts`（stub UI 无需修改）
- 所有命令的 `action` 实现（Phase 1 不修改任何命令行为）

---

## 10. 行为影响分析

### 10.1 变化汇总

| 场景                                 | 旧行为                       | 新行为                                                   | 性质        |
| ------------------------------------ | ---------------------------- | -------------------------------------------------------- | ----------- |
| non-interactive 下执行 `/init`       | ✅ 允许（白名单）            | ✅ 允许（`commandType: local`）                          | 无变化      |
| non-interactive 下执行 `/summary`    | ✅ 允许                      | ✅ 允许                                                  | 无变化      |
| non-interactive 下执行 `/compress`   | ✅ 允许                      | ✅ 允许                                                  | 无变化      |
| non-interactive 下执行 `/btw`        | ✅ 允许                      | ✅ 允许                                                  | 无变化      |
| non-interactive 下执行 `/bug`        | ✅ 允许                      | ✅ 允许                                                  | 无变化      |
| non-interactive 下执行 `/context`    | ✅ 允许                      | ✅ 允许                                                  | 无变化      |
| non-interactive 下执行 `/model`      | ❌ unsupported               | ❌ unsupported（`commandType: local-jsx`）               | 无变化      |
| non-interactive 下执行 file command  | ✅ 允许（CommandKind.FILE）  | ✅ 允许（`commandType: prompt`）                         | 无变化      |
| non-interactive 下执行 bundled skill | ✅ 允许（CommandKind.SKILL） | ✅ 允许（`commandType: prompt`）                         | 无变化      |
| non-interactive 下执行 MCP prompt    | ❌ 被 CommandKind 拦截       | ✅ 允许（`commandType: prompt`）                         | **Bug fix** |
| non-interactive 下执行 `/export`     | ❌ 不在白名单                | ❌ 不允许（`commandType: local`，默认 interactive only） | 无变化      |
| non-interactive 下执行 `/memory`     | ❌ 不在白名单                | ❌ 不允许（`commandType: local`，默认 interactive only） | 无变化      |
| non-interactive 下执行 `/plan`       | ❌ 不在白名单                | ❌ 不允许（`commandType: local`，默认 interactive only） | 无变化      |

> **关于 `local` 命令的保守默认策略**：`commandType: 'local'` 的默认 `supportedModes` 为 `['interactive']`，这与 Claude Code 的设计一致——`local` 类型命令需要显式声明 `supportsNonInteractive: true` 才能在非交互模式下运行。Phase 1 中白名单内的 6 个命令（`init`、`summary`、`compress`、`btw`、`bug`、`context`）通过显式声明 `supportedModes: ['interactive', 'non_interactive', 'acp']` 来等价替换原白名单效果。Phase 2 中需要扩展的命令（如 `/export`、`/memory`、`/plan`）在验证 action 实现 headless-friendly 之后，再逐个解锁。

---

## 10.2 Phase 2 模式差异命令：双注册模式

对于 Phase 2 中需要"交互模式有 UI，非交互模式有文本输出"的命令（如 `/model`），应采用 **双注册模式**，而非在单个命令的 `action` 内部分支。

这是 Claude Code 的标准模式，以 `/context` 为例（参见 `src/commands/context/index.ts`）：两个同名 `Command` 对象，一个 `local-jsx` 仅 interactive，另一个 `local` 仅 non-interactive，通过 `isEnabled()` 互斥。

Qwen Code 在 Phase 2 中应采用等价方式，以 `supportedModes` 替代 `isEnabled()` 实现互斥：

```typescript
// ① 交互模式版：local-jsx，仅 interactive
export const modelCommandInteractive: SlashCommand = {
  name: 'model',
  kind: CommandKind.BUILT_IN,
  commandType: 'local-jsx',
  supportedModes: ['interactive'], // 显式限定
  // action: 打开 dialog 选择 model
};

// ② 非交互/acp 版：local，显式开放给 headless 调用者
export const modelCommandHeadless: SlashCommand = {
  name: 'model',
  kind: CommandKind.BUILT_IN,
  commandType: 'local',
  supportedModes: ['non_interactive', 'acp'], // 显式限定
  // action: 读取/设置 model，返回 message（纯文本）
};
```

两个对象同名，`supportedModes` 互斥，`filterCommandsForMode` 自动选择正确版本。与 Claude Code 的 `isEnabled()` 互斥相比，`supportedModes` 过滤更显式、更易测试，且不需要运行时环境检测。

**Phase 1 不实现任何双注册命令**，该模式仅作为 Phase 2 的实施规范预留在此。

---

## 11. 测试策略

### 11.1 新增工具函数测试

在 `packages/cli/src/services/commandUtils.test.ts`（新文件）中：

```
/* Detailed source-code truncated for AI context efficiency. */
```

### 11.2 更新 `nonInteractiveCliCommands.test.ts`

- 删除对 `ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE` 的所有引用
- 删除对 `allowedBuiltinCommandNames` 参数的测试用例
- 新增：验证 commandType: local 的命令在 non-interactive 下通过过滤
- 新增：验证 commandType: local-jsx 的命令在 non-interactive 下被过滤
- 保留：验证 file command / skill command 在 non-interactive 下通过过滤

### 11.3 更新 `CommandService.test.ts`

- 新增 `getCommandsForMode` 的测试用例
- 新增 `getModelInvocableCommands` 的测试用例

### 11.4 各 Loader 测试

- `BuiltinCommandLoader.test.ts`：验证所有命令都有 `source: 'builtin-command'`
- `BundledSkillLoader.test.ts`：验证 `source: 'bundled-skill'` 和 `modelInvocable: true`
- `FileCommandLoader.test.ts`：验证用户命令有 `source: 'skill-dir-command'`，插件命令有 `source: 'plugin-command'`
- `McpPromptLoader.test.ts`：验证 `source: 'mcp-prompt'` 和 `modelInvocable: true`

---

## 12. 实施顺序

建议按以下顺序实施，每步可独立 commit 和 review：

**Step 1**（~30min）：修改 `types.ts`，新增 `ExecutionMode`、`CommandSource`、`CommandType` 和 `SlashCommand` 新字段
→ 纯类型变更，TypeScript 编译检查

**Step 2**（~1h）：新建 `commandUtils.ts`，实现 `getEffectiveSupportedModes` 和 `filterCommandsForMode`，同步新建 `commandUtils.test.ts`
→ 单元测试覆盖核心逻辑

**Step 3**（~1h）：重构 `nonInteractiveCliCommands.ts`，删除白名单，引入 `filterCommandsForMode`，更新函数签名
→ 行为等价（Phase 1 保守策略：local 类命令显式写 `supportedModes: ['interactive']`）

**Step 4**（~30min）：更新 `CommandService.ts`，新增两个方法

**Step 5**（~2h）：为所有 built-in 命令文件添加 `commandType` 声明
→ 逐个确认分类正确性

**Step 6**（~1.5h）：更新所有 Loader，注入 `source`、`sourceLabel`、`commandType`、`modelInvocable`

**Step 7**（~30min）：更新 `Session.ts` 的调用签名

**Step 8**（~1h）：运行所有测试，修复失败用例，更新快照

**Step 9**（~30min）：CR 自查：确认白名单已完全移除，无遗漏调用

---

## 13. 验收 Checklist

- [ ] TypeScript 编译无错误（`npm run typecheck`）
- [ ] `npm run lint` 无新增 lint 错误
- [ ] 所有现有测试通过（`cd packages/cli && npx vitest run`）
- [ ] `commandUtils.test.ts` 新增测试全部通过
- [ ] `getEffectiveSupportedModes` 覆盖所有 7 种 case
- [ ] `filterCommandsForMode` 覆盖 interactive / non_interactive / acp 三种模式
- [ ] `ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE` 在整个代码库中无任何引用（`grep` 验证）
- [ ] `filterCommandsForNonInteractive` 函数在整个代码库中无任何引用
- [ ] 所有 built-in 命令有 `commandType` 字段
- [ ] 所有 Loader 输出的命令有 `source` 和 `sourceLabel` 字段
- [ ] `BundledSkillLoader` / `FileCommandLoader`（用户命令）/ `McpPromptLoader` 输出的命令 `modelInvocable: true`
- [ ] `BuiltinCommandLoader` 输出的命令 `modelInvocable: false`
- [ ] `CommandService.getCommandsForMode('non_interactive')` 返回与重构前等价的命令集
- [ ] MCP prompt 命令在 non-interactive 模式下不再被错误拦截

---

### Design Slash Command Phase2 Technical Design (`docs/design/slash-command/phase2-technical-design.md`)

# Phase 2 技术设计文档：能力扩展

## 1. 设计目标与约束

### 1.1 目标

- 将 13 个 built-in 命令的 `supportedModes` 扩展到包含 `non_interactive` 和/或 `acp`
- 确保每个扩展命令在 ACP/non-interactive 路径下返回适合 IDE 消费的文本内容
- 打通 prompt command 的模型调用通路（`SkillTool` 消费 `getModelInvocableCommands()`）
- 实现 mid-input slash command 基础检测

### 1.2 硬性约束

- **interactive 路径零退化**：所有扩展命令的现有 interactive 行为严格不变，只在 action 内部新增模式分支，不触碰 interactive 路径代码
- **实现策略：模式分支，而非双注册**：13 个命令均采用在 `action` 内部增加 `executionMode` 判断的方式，不使用 Phase 1 设计文档 §10.2 描述的双注册模式（双注册仅在 interactive 和 non-interactive 逻辑差异极大时才有必要，本阶段命令复杂度不达到该门槛）
- **ACP 消息格式**：ACP 路径返回的文本内容不含 ANSI 样式，以 Markdown 或纯文本为宜，面向 IDE 插件消费
- **跳过环境相关副作用**：打开浏览器（`open()`）、操作剪贴板（`copyToClipboard()`）等依赖图形环境的操作，在 non-interactive/ACP 路径下必须跳过

---

## 2. Phase 1 完成后的基础状态

Phase 1 结束后的架构要点（Phase 2 直接在此基础上扩展）：

- `commandType` 字段已从 `SlashCommand` 接口中删除，所有命令改用显式 `supportedModes`
- `getEffectiveSupportedModes()` 为两级推断：显式 `supportedModes` → `CommandKind` 兜底
- `CommandService.getCommandsForMode(mode)` 取代原 `ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE` 白名单
- `btw`、`bug`、`compress`、`context`、`init`、`summary` 已在 Phase 1 中扩展到全模式，**不在本阶段列表中**
- `createNonInteractiveUI()` 中各方法均为 no-op：`addItem`、`clear`、`setDebugMessage`、`setPendingItem`、`reloadCommands` 均静默忽略调用

---

## 3. 变更范围总览

本阶段共涉及 13 个命令，按实现复杂度分为四类：

| 类别       | 命令                                         | 变更要点                                                                             |
| ---------- | -------------------------------------------- | ------------------------------------------------------------------------------------ |
| **A 类**   | `export`                                     | 只改 `supportedModes`，action 所有路径已返回合法类型                                 |
| **仅交互** | `plan`、`statusline`                         | 设计决策：这两个命令语义上与交互界面紧密耦合，保持 `supportedModes: ['interactive']` |
| **A+ 类**  | `language`                                   | 改 `supportedModes` + 少量 non-interactive 分支处理                                  |
| **仅交互** | `copy`、`restore`                            | 设计决策：剥贴板和快照恢复本质上是交互操作，保持 `supportedModes: ['interactive']`   |
| **A' 类**  | `model`、`approval-mode`                     | 有参数路径已返回 `message`，无参数路径需新增 non-interactive 分支（现触发 dialog）   |
| **B 类**   | `about`、`stats`、`insight`、`docs`、`clear` | action 所有路径均无返回值或调用 `addItem`/`clear`，需新增完整 non-interactive 分支   |

---

## 4. A 类：只改 `supportedModes`

这三个命令的所有 `action` 路径已经返回 `message` 或 `submit_prompt`，完全无 UI 依赖，`handleCommandResult` 可直接处理。

### 4.1 `/export`（及子命令）

**当前状态**：`supportedModes: ['interactive']`，所有子命令 action 均返回 `MessageActionReturn`。

**变更**：将父命令及所有四个子命令（`md`、`html`、`json`、`jsonl`）的 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。

**ACP 消息内容**：action 现有返回内容已包含完整文件路径（如 `Session exported to markdown: qwen-export-2024-01-01T12-00-00.md`），对 IDE 消费友好，无需修改文本。

> **注意**：`/export` 父命令本身没有 `action`，只有子命令。将父命令 `supportedModes` 改为全模式后，`parseSlashCommand` 能够匹配子命令路由，但若用户只输入 `/export` 不带子命令，`commandToExecute.action` 为 undefined，`handleSlashCommand` 返回 `no_command`，调用方会显示可用子命令提示。这是预期行为。

### 4.2 `/plan`

**当前状态**：`supportedModes: ['interactive']`，action 所有路径返回 `MessageActionReturn` 或 `SubmitPromptActionReturn`。

**设计决策**：`/plan` 是引导用户进行多轮交互规划的命令，语义上与交互界面紧密耦合。经讨论决定保持 `supportedModes: ['interactive']`，不扩展至 non-interactive/acp 模式。

### 4.3 `/statusline`

**当前状态**：`supportedModes: ['interactive']`，action 始终返回 `SubmitPromptActionReturn`（将 subagent 调用 prompt 提交给模型）。

**设计决策**：`/statusline` 是触发 subagent 对当前状态进行总结的命令，语义上与交互界面紧密耦合。经讨论决定保持 `supportedModes: ['interactive']`，不扩展至 non-interactive/acp 模式。

---

## 5. A+ 类：少量 non-interactive 分支处理

### 5.1 `/language`

**当前状态**：action 所有路径均返回 `MessageActionReturn`（读取/设置语言设置）。

**需要处理的副作用**：`setUiLanguage()` 内调用 `context.ui.reloadCommands()`，在非交互 UI 中已是 no-op，无需额外处理。

**变更**：

- 将父命令及子命令（`ui`、`output`，以及 `SUPPORTED_LANGUAGES` 动态生成的子命令）的 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。
- action 无需添加模式分支，现有返回文本已适合机器消费。

**ACP 语义说明**：在 non-interactive（单次调用）中执行 `/language ui zh-CN` 会修改持久化设置（写入 settings 文件），该变更对后续 session 生效，本次 session 内 i18n 也立即生效。这与用户预期一致。

### 5.2 `/copy`

**当前状态**：action 调用 `copyToClipboard()`，在 ACP/headless 环境中可能抛出异常或无声失败（clipboard 不可用）。

**变更**：

1. 将 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。
2. 在 action 内新增模式分支：

```typescript
// 获取 last AI message（现有逻辑，可复用）
if (context.executionMode !== 'interactive') {
  // 非交互/ACP：跳过剪贴板，返回内容本身
  if (!lastAiOutput) {
    return {
      type: 'message',
      messageType: 'info',
      content: 'No output in history.',
    };
  }
  return {
    type: 'message',
    messageType: 'info',
    content: lastAiOutput,
  };
}
// interactive 路径：原有剪贴板逻辑不变
await copyToClipboard(lastAiOutput);
return {
  type: 'message',
  messageType: 'info',
  content: 'Last output copied to the clipboard',
};
```

**ACP 语义**：IDE 收到最后一条模型输出的原文，可自行决定是否写入剪贴板或展示给用户。

### 5.3 `/restore`

**当前状态**：`supportedModes: ['interactive']`。

**设计决策**：快照恢复进一步会重新执行工具调用，语义上与交互界面紧密耦合。经讨论决定保持 `supportedModes: ['interactive']`，不扩展至 non-interactive/acp 模式。

**ACP 语义**：checkpoint 的 git 状态恢复和 gemini client history 设置均作为副作用执行；IDE 收到确认消息后可提示用户"状态已恢复"，工具重执行由 IDE 自行决定是否触发。

---

## 6. A' 类：无参数 dialog 路径的 non-interactive 处理

### 6.1 `/model`

**当前状态**：

| 输入                             | 当前行为                                                                         |
| -------------------------------- | -------------------------------------------------------------------------------- |
| `/model`（无参数）               | → `{ type: 'dialog', dialog: 'model' }`（non-interactive 下变 unsupported）      |
| `/model <model-id>`              | 未实现（只有 `--fast` 分支）                                                     |
| `/model --fast`（无 model name） | → `{ type: 'dialog', dialog: 'fast-model' }`（non-interactive 下变 unsupported） |
| `/model --fast <model-id>`       | → `MessageActionReturn` ✅                                                       |

**变更**：

1. 将 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。
2. 在 action 内各 dialog 路径前插入 non-interactive 分支：

```typescript
// 无参数路径（原返回 dialog: 'model'）
if (!args.trim()) {
  if (context.executionMode !== 'interactive') {
    const currentModel = config.getModel() ?? 'unknown';
    return {
      type: 'message',
      messageType: 'info',
      content: `Current model: ${currentModel}\nUse "/model <model-id>" to switch models.`,
    };
  }
  return { type: 'dialog', dialog: 'model' };
}

// --fast 无参数路径（原返回 dialog: 'fast-model'）
if (args.startsWith('--fast') && !modelName) {
  if (context.executionMode !== 'interactive') {
    const fastModel = context.services.settings?.merged?.fastModel ?? 'not set';
    return {
      type: 'message',
      messageType: 'info',
      content: `Current fast model: ${fastModel}\nUse "/model --fast <model-id>" to set fast model.`,
    };
  }
  return { type: 'dialog', dialog: 'fast-model' };
}
```

**ACP 语义**：IDE 展示当前模型名称，供用户参考；切换模型通过带参数调用实现（`/model <model-id>`）。

> **注意**：`/model <model-id>`（不带 `--fast`）目前没有实现设置当前 session 模型的逻辑，只有 `--fast <model-id>` 有。如果 Phase 2 要支持 ACP 下切换主模型，需要同步实现 `/model <model-id>` 的 set 逻辑。本设计预留此路径但标记为 Phase 2 可选项，优先保证"查看当前模型"的 read-only 路径。

### 6.2 `/approval-mode`

**当前状态**：

| 输入                       | 当前行为                                                                            |
| -------------------------- | ----------------------------------------------------------------------------------- |
| `/approval-mode`（无参数） | → `{ type: 'dialog', dialog: 'approval-mode' }`（non-interactive 下变 unsupported） |
| `/approval-mode <mode>`    | → `MessageActionReturn` ✅                                                          |
| `/approval-mode <invalid>` | → `MessageActionReturn`（error）✅                                                  |

**变更**：

1. 将 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。
2. 在无参数路径（`!args.trim()`）插入 non-interactive 分支：

```typescript
if (!args.trim()) {
  if (context.executionMode !== 'interactive') {
    const currentMode = config?.getApprovalMode() ?? 'unknown';
    return {
      type: 'message',
      messageType: 'info',
      content: `Current approval mode: ${currentMode}\nAvailable modes: ${APPROVAL_MODES.join(', ')}\nUse "/approval-mode <mode>" to change.`,
    };
  }
  return { type: 'dialog', dialog: 'approval-mode' };
}
```

---

## 7. B 类：需要完整 non-interactive 分支

这五个命令的 action 在 interactive 模式下通过 `context.ui.addItem()` 渲染 React 组件或调用 `context.ui.clear()`，返回值为 `void`。在 non-interactive 中，这些调用均为 no-op，导致 `handleSlashCommand` 将无返回值处理为 `"Command executed successfully."`，无实际内容输出。

**实现原则**：在 action **顶部**检查 `executionMode`，非 interactive 时 **提前 return** 包含实际内容的 `message`，interactive 路径代码完全不触碰。

### 7.1 `/about`（altName: `status`）

**数据来源**：`getExtendedSystemInfo(context)` 返回 `ExtendedSystemInfo`，包含：`cliVersion`、`osPlatform`、`osArch`、`osRelease`、`nodeVersion`、`modelVersion`、`selectedAuthType`、`ideClient`、`sessionId`、`memoryUsage`、`baseUrl`、`apiKeyEnvKey`、`gitCommit`、`fastModel`。所有字段在 non-interactive 中均可获取（context.services.config 和 settings 均已注入）。

**变更**：

1. 将 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。
2. 在 `getExtendedSystemInfo` 调用后，interactive 路径之前插入模式分支：

```typescript
action: async (context) => {
  const systemInfo = await getExtendedSystemInfo(context);

  if (context.executionMode !== 'interactive') {
    const lines = [
      `Qwen Code v${systemInfo.cliVersion}`,
      `Model: ${systemInfo.modelVersion}`,
      `Fast Model: ${systemInfo.fastModel ?? 'not set'}`,
      `Auth: ${systemInfo.selectedAuthType}`,
      `Platform: ${systemInfo.osPlatform} ${systemInfo.osArch} (${systemInfo.osRelease})`,
      `Node.js: ${systemInfo.nodeVersion}`,
      `Session: ${systemInfo.sessionId}`,
      ...(systemInfo.gitCommit ? [`Git commit: ${systemInfo.gitCommit}`] : []),
      ...(systemInfo.ideClient ? [`IDE: ${systemInfo.ideClient}`] : []),
    ];
    return {
      type: 'message',
      messageType: 'info',
      content: lines.join('\n'),
    };
  }

  // interactive 路径：原有 addItem 逻辑不变
  const aboutItem: Omit<HistoryItemAbout, 'id'> = { type: MessageType.ABOUT, systemInfo };
  context.ui.addItem(aboutItem, Date.now());
},
```

### 7.2 `/stats`（及子命令 `model`、`tools`）

**数据来源**：`context.session.stats`（`SessionStatsState`）包含 `sessionStartTime`、`metrics`（`SessionMetrics`：`models`、`tools`、`files`）、`promptCount`。在 non-interactive 中，`sessionStartTime` 为当前调用时刻，`metrics` 来自 `uiTelemetryService.getMetrics()`（本次调用的累积值，通常为零），`promptCount` 为 1。

**变更**：

1. 将父命令 `stats` 及子命令 `model`、`tools` 的 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。
2. 父命令和每个子命令的 action 均插入模式分支，提前返回文本格式统计：

```typescript
// /stats 主命令
action: (context) => {
  if (context.executionMode !== 'interactive') {
    const now = new Date();
    const { sessionStartTime, promptCount, metrics } = context.session.stats;
    if (!sessionStartTime) {
      return { type: 'message', messageType: 'error', content: 'Session start time unavailable.' };
    }
    const wallDuration = now.getTime() - sessionStartTime.getTime();

    // 汇总所有 model 的 token 数
    let totalPromptTokens = 0, totalCandidateTokens = 0, totalRequests = 0;
    for (const modelMetrics of Object.values(metrics.models)) {
      totalPromptTokens += modelMetrics.tokens.prompt;
      totalCandidateTokens += modelMetrics.tokens.candidates;
      totalRequests += modelMetrics.api.totalRequests;
    }

    const lines = [
      `Session duration: ${formatDuration(wallDuration)}`,
      `Prompts: ${promptCount}`,
      `API requests: ${totalRequests}`,
      `Tokens — prompt: ${totalPromptTokens}, output: ${totalCandidateTokens}`,
      `Tool calls: ${metrics.tools.totalCalls} (${metrics.tools.totalSuccess} ok, ${metrics.tools.totalFail} fail)`,
      `Files: +${metrics.files.totalLinesAdded} / -${metrics.files.totalLinesRemoved} lines`,
    ];
    return { type: 'message', messageType: 'info', content: lines.join('\n') };
  }

  // interactive 路径：原有 addItem 逻辑不变
  const statsItem: HistoryItemStats = { type: MessageType.STATS, duration: formatDuration(wallDuration) };
  context.ui.addItem(statsItem, Date.now());
},
```

子命令 `model` 和 `tools` 也各自插入模式分支，返回对应维度的文本统计（model 维度按 model name 列出 token 用量；tools 维度列出各 tool 调用次数）。

**说明**：在 non-interactive 单次调用中，metrics 通常为零（新 session），但结构完整，不影响格式。ACP Session 中可能有累积值，有实际意义。

### 7.3 `/insight`

**当前状态**：action 返回 `void`，通过 `addItem` 展示进度和结果，最后调用 `open(outputPath)` 打开浏览器。核心逻辑是 `insightGenerator.generateStaticInsight()` 生成 HTML 文件。

**变更**：

1. 将 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。
2. 按 `executionMode` 三路分叉：
   - `non_interactive`：同步生成，忽略进度回调，不开浏览器，直接返回 `message`（文件路径）
   - `acp`：异步启动生成，通过 `stream_messages` 将进度（`encodeInsightProgressMessage`）和完成（`encodeInsightReadyMessage`）推送给 IDE
   - `interactive`：原有 `addItem` + `setPendingItem` + `open()` 逻辑不变

```typescript
// non_interactive 路径
if (context.executionMode === 'non_interactive') {
  const outputPath = await insightGenerator.generateStaticInsight(
    projectsDir,
    () => {}, // no-op progress
  );
  return {
    type: 'message',
    messageType: 'info',
    content: t('Insight report generated at: {{path}}', { path: outputPath }),
  };
}

// acp 路径：stream_messages
if (context.executionMode === 'acp') {
  // ... 构造 streamMessages async generator，yield encodeInsightProgressMessage / encodeInsightReadyMessage ...
  return { type: 'stream_messages', messages: streamMessages() };
}

// interactive 路径：原有实现不变
```

**设计理由**：`non_interactive` 模式（CLI 管道）不支持 `stream_messages`，只能返回单条 `message`；ACP 模式（IDE 插件）能消费 `stream_messages` 并实时展示进度，因此为其保留 streaming 路径。

**ACP 消息格式**：`encodeInsightProgressMessage(stage, progress, detail?)` 产生 IDE 可解析的进度条消息；`encodeInsightReadyMessage(outputPath)` 通知 IDE 文件已就绪，由 IDE 决定如何展示链接。

### 7.4 `/docs`

**当前状态**：action 返回 `void`，通过 `addItem` 显示消息并调用 `open(docsUrl)` 打开浏览器。有一个 `SANDBOX` 环境变量分支（沙盒下只 addItem，不开浏览器）。

**变更**：

1. 将 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。
2. 修改 action 返回类型为 `Promise<void | MessageActionReturn>`。
3. 在 action 开头插入 non-interactive 分支：

```typescript
action: async (context) => {
  const langPath = getCurrentLanguage()?.startsWith('zh') ? 'zh' : 'en';
  const docsUrl = `https://qwenlm.github.io/qwen-code-docs/${langPath}`;

  if (context.executionMode !== 'interactive') {
    // 非交互/ACP：直接返回 URL，不打开浏览器，不调用 addItem
    return {
      type: 'message',
      messageType: 'info',
      content: `Qwen Code documentation: ${docsUrl}`,
    };
  }

  // interactive 路径：原有 SANDBOX 判断 + addItem + open() 不变
  if (process.env['SANDBOX'] && ...) {
    context.ui.addItem(...);
  } else {
    context.ui.addItem(...);
    await open(docsUrl);
  }
},
```

### 7.5 `/clear`（altNames: `reset`、`new`）

**当前状态**：action 执行以下操作并返回 `void`：

1. `config.getHookSystem()?.fireSessionEndEvent()` — 触发 hook（有副作用）
2. `config.startNewSession()` — 开始新 session ID（有副作用）
3. `uiTelemetryService.reset()` — 重置 telemetry 计数器（有副作用）
4. `skillTool.clearLoadedSkills()` — 清除 skill 缓存（有副作用）
5. `context.ui.clear()` — 清空终端 UI（**UI 副作用，non-interactive 下为 no-op**）
6. `geminiClient.resetChat()` — 重置 chat 历史（有副作用）
7. `config.getHookSystem()?.fireSessionStartEvent()` — 触发 hook（有副作用）

**non-interactive/ACP 语义分析**：

- `ui.clear()` 在 non-interactive 中已是 no-op，不需要处理
- `geminiClient.resetChat()`：在 ACP Session 中是有意义的副作用（清空 chat 历史），应保留；在 non-interactive 单次调用中，每次调用都是全新 session，`resetChat` 语义重复但无害
- `config.startNewSession()`：在 ACP 中有意义（开始新的 session ID）；在 non-interactive 单次调用中同样语义重复但无害
- `fireSessionEndEvent` / `fireSessionStartEvent`：在 ACP 中有意义（触发 hook）

**决策**：non-interactive/ACP 路径保留所有有意义的副作用（resetChat、startNewSession、hook events），仅跳过 `ui.clear()`（已是 no-op）并返回上下文边界标记 message。

**变更**：

1. 将 `supportedModes` 改为 `['interactive', 'non_interactive', 'acp']`。
2. 修改 action 返回类型为 `Promise<void | MessageActionReturn>`。
3. 在 action 内，`context.ui.clear()` 调用后（或替代它）根据模式分支：

```typescript
action: async (context, _args) => {
  const { config } = context.services;

  if (config) {
    config.getHookSystem()?.fireSessionEndEvent(SessionEndReason.Clear).catch(...);

    const newSessionId = config.startNewSession();
    uiTelemetryService.reset();

    const skillTool = config.getToolRegistry()?.getAllTools().find(...);
    if (skillTool instanceof SkillTool) skillTool.clearLoadedSkills();

    if (newSessionId && context.session.startNewSession) {
      context.session.startNewSession(newSessionId);
    }

    // ui.clear() 在非交互下已是 no-op，但依然调用（不需要条件分支）
    context.ui.clear();

    const geminiClient = config.getGeminiClient();
    if (geminiClient) {
      await geminiClient.resetChat();
    }

    config.getHookSystem()?.fireSessionStartEvent(...).catch(...);
  } else {
    context.ui.clear();
  }

  // 根据模式决定返回值
  if (context.executionMode !== 'interactive') {
    return {
      type: 'message',
      messageType: 'info',
      content: 'Context cleared. Previous messages are no longer in context.',
    };
  }
  // interactive 路径：void（不返回，React UI 由 ui.clear() 驱动更新）
},
```

**ACP 语义**：IDE 收到上下文边界标记后，可将其作为 session 分隔符展示（如"新会话开始"提示），并清空本地 chat 历史缓存。

---

## 8. `handleCommandResult` 变更

**结论：无需修改。**

Phase 2 所有命令变更后，non-interactive/ACP 路径的返回类型均为 `message` 或 `submit_prompt`，均已在 `handleCommandResult` 的 switch 中正确处理。

---

## 9. `createNonInteractiveUI()` 变更

**结论：无需修改。**

当前 no-op 实现已足够。`addItem`、`clear`、`setPendingItem` 等 no-op 在 B 类命令的 non-interactive 路径中不会被调用（因为提前 return）；interactive 路径中不受影响。

---

## 10. Phase 2.2：prompt command 模型调用打通

Phase 1 中 `CommandService.getModelInvocableCommands()` 已实现，`BundledSkillLoader`、`FileCommandLoader`（用户/项目命令）、`McpPromptLoader` 已设置 `modelInvocable: true`。

Phase 2.2 的工作是将 `SkillTool` 从只消费 `SkillManager.listSkills()` 改为同时消费 `CommandService.getModelInvocableCommands()`，统一模型可调用命令的入口。

**变更文件**：`packages/core/src/tools/SkillTool.ts`（或对应路径）

**具体变更**：

1. `SkillTool` 在初始化时接收 `CommandService`（或其 `getModelInvocableCommands()` 的结果）作为依赖注入
2. 在构建 tool description 时，合并 `listSkills()` 和 `getModelInvocableCommands()` 的结果
3. 确保 built-in commands（`modelInvocable: false`）不出现在 tool description 中

> **注**：`SkillTool` 的具体实现依赖 `packages/core` 内部架构，详细设计在本文档中仅描述接口变更，实现细节需结合 core 包的现有结构确定。

---

## 11. Phase 2.3：mid-input slash command 检测（基础版）

在 `InputPrompt` 组件中检测光标附近的 slash token（不限于行首），触发补全菜单。

**检测规则**：

- 当光标前存在以 `/` 开头、不含空格的 token 时，触发命令补全
- 补全候选来自 `getCommandsForMode('interactive')` 的可见命令列表
- 补全菜单展示命令名 + description（不含 argumentHint 等，Phase 3 补充）

> 本功能为 UI 层变更，属于 Phase 2.3 独立子任务，不影响其他 Phase 2.1/2.2 的实施。

---

## 12. 文件变更总览

### 12.1 命令文件变更（Phase 2.1）

| 文件                     | 变更类型 | 具体内容                                                                                                                             |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `exportCommand.ts`       | A 类     | 父命令 + 4 个子命令：`supportedModes` → all modes                                                                                    |
| `planCommand.ts`         | 仅交互   | 设计决策：保持 `supportedModes: ['interactive']`，未变更                                                                             |
| `statuslineCommand.ts`   | 仅交互   | 设计决策：保持 `supportedModes: ['interactive']`，未变更                                                                             |
| `languageCommand.ts`     | A+ 类    | 父命令 + `ui`/`output` 子命令 + 动态 language 子命令：`supportedModes` → all modes                                                   |
| `copyCommand.ts`         | 仅交互   | 设计决策：保持 `supportedModes: ['interactive']`，未变更                                                                             |
| `restoreCommand.ts`      | 仅交互   | 设计决策：保持 `supportedModes: ['interactive']`，未变更                                                                             |
| `modelCommand.ts`        | A' 类    | `supportedModes` → all modes + 无参数/无 fast model 路径新增非交互分支                                                               |
| `approvalModeCommand.ts` | A' 类    | `supportedModes` → all modes + 无参数路径新增非交互分支                                                                              |
| `aboutCommand.ts`        | B 类     | `supportedModes` → all modes + 非交互路径返回 `message`（版本/模型/环境摘要）                                                        |
| `statsCommand.ts`        | B 类     | `supportedModes` → all modes + 非交互路径返回 `message`（stats 文本）；子命令同步处理                                                |
| `insightCommand.ts`      | B 类     | `supportedModes` → all modes + `non_interactive` 路径同步生成返回 `message`（文件路径）；`acp` 路径返回 `stream_messages` 带进度推送 |
| `docsCommand.ts`         | B 类     | `supportedModes` → all modes + 非交互路径返回 `message`（文档 URL），不打开浏览器                                                    |
| `clearCommand.ts`        | B 类     | `supportedModes` → all modes + action 末尾根据模式返回 `message` 或 `void`                                                           |

### 12.2 其他文件变更

| 文件                                                | 变更内容                                                          |
| --------------------------------------------------- | ----------------------------------------------------------------- |
| `packages/core/src/tools/SkillTool.ts`              | Phase 2.2：接入 `getModelInvocableCommands()`（详细设计另行确定） |
| `packages/cli/src/ui/InputPrompt.tsx`（或同等组件） | Phase 2.3：mid-input slash 检测逻辑                               |

### 12.3 不变的文件

- `packages/cli/src/nonInteractiveCliCommands.ts`（`handleCommandResult`、`handleSlashCommand` 无需修改）
- `packages/cli/src/ui/noninteractive/nonInteractiveUi.ts`（stub UI 无需修改）
- `packages/cli/src/services/commandUtils.ts`（`filterCommandsForMode`、`getEffectiveSupportedModes` 无需修改）
- `packages/cli/src/services/CommandService.ts`（`getCommandsForMode`、`getModelInvocableCommands` 已在 Phase 1 实现）

---

## 13. 测试策略

### 13.1 命令单元测试

为每个变更的命令在同目录下新增或更新测试文件（`*.test.ts`），覆盖以下 case：

**A/A+ 类命令**（`export`、`language`）：

- `supportedModes` 正确包含 `non_interactive` 和 `acp`
- 在 `executionMode: 'non_interactive'` 下，action 返回 `MessageActionReturn` 或 `SubmitPromptActionReturn`，不调用 `ui.addItem` 或 `ui.clear`
- Interactive 路径行为与重构前完全一致（快照测试）

**仅交互命令**（`plan`、`statusline`、`copy`、`restore`）：

- `supportedModes` 为 `['interactive']`，这是设计决策
- 验证 non-interactive 下执行时正确返回 `unsupported`

**A' 类命令**（`model`、`approval-mode`）：

- 无参数 + `executionMode: 'non_interactive'` → 返回当前状态 `message`，不返回 `dialog`
- 有参数 + `executionMode: 'non_interactive'` → 原有 `message` 逻辑正常执行
- Interactive 路径：无参数 → `dialog`，有参数 → `message`（不变）

**B 类命令**（`about`、`stats`、`insight`、`docs`、`clear`）：

- `executionMode: 'non_interactive'` 下，action 返回 `MessageActionReturn`，不调用任何 `ui.*` 方法
- 返回的 `content` 字符串包含预期的关键字段（版本号、模型名、URL 等）
- Interactive 路径：`ui.addItem` 被调用，`action` 返回 `void`（不变）

**`clear` 的特殊 case**：

- `executionMode: 'non_interactive'` 下，`geminiClient.resetChat()` 仍被调用（副作用保留）
- 返回上下文边界 `message`，内容为 `'Context cleared. Previous messages are no longer in context.'`

### 13.2 集成测试（`handleSlashCommand`）

在 `nonInteractiveCli.test.ts` 或新建的集成测试文件中：

- `handleSlashCommand('/about', ...)` 在 non-interactive 模式下返回 `{ type: 'message', content: 包含版本号 }`
- `handleSlashCommand('/stats', ...)` 在 non-interactive 模式下返回 `{ type: 'message', content: 包含 'Session duration' }`
- `handleSlashCommand('/docs', ...)` 在 non-interactive 模式下返回 `{ type: 'message', content: 包含 'qwenlm.github.io' }`
- `handleSlashCommand('/clear', ...)` 在 non-interactive 模式下返回 `{ type: 'message', content: 'Context cleared.' }`
- `handleSlashCommand('/plan', ...)` 在 non-interactive 模式下返回 `unsupported`（仅交互命令）
- 现有 non-interactive 命令（`btw`、`bug` 等）行为无退化

### 13.3 `commandUtils` 测试

`commandUtils.test.ts` 中新增（或已有的测试继续覆盖）：

- 扩展后的命令（`export`、`language` 等）均能通过 `filterCommandsForMode(commands, 'non_interactive')` 和 `filterCommandsForMode(commands, 'acp')` 的过滤
- 仅交互命令（`plan`、`statusline`、`copy`、`restore`）在 `filterCommandsForMode(commands, 'non_interactive')` 下被正确过滤掉

---

## 14. 行为影响分析

| 场景                                         | Phase 2 前行为                                            | Phase 2 后行为                     | 性质               |
| -------------------------------------------- | --------------------------------------------------------- | ---------------------------------- | ------------------ |
| non-interactive 下执行 `/export md`          | ❌ unsupported（被过滤）                                  | ✅ 返回文件路径 message            | 能力扩展           |
| non-interactive 下执行 `/plan <task>`        | ❌ unsupported                                            | ❌ unsupported（设计决策：仅交互） | 不变               |
| non-interactive 下执行 `/statusline`         | ❌ unsupported                                            | ❌ unsupported（设计决策：仅交互） | 不变               |
| non-interactive 下执行 `/language ui zh-CN`  | ❌ unsupported                                            | ✅ 设置语言，返回确认 message      | 能力扩展           |
| non-interactive 下执行 `/copy`               | ❌ unsupported                                            | ❌ unsupported（设计决策：仅交互） | 不变               |
| non-interactive 下执行 `/restore`（无参数）  | ❌ unsupported                                            | ❌ unsupported（设计决策：仅交互） | 不变               |
| non-interactive 下执行 `/restore <id>`       | ❌ unsupported                                            | ❌ unsupported（设计决策：仅交互） | 不变               |
| non-interactive 下执行 `/model`              | ❌ unsupported（dialog）                                  | ✅ 返回当前模型名称                | 能力扩展           |
| non-interactive 下执行 `/model <id>`         | ❌ unsupported                                            | 🔄 Phase 2 可选：实现切换逻辑      | 能力扩展（可选）   |
| non-interactive 下执行 `/approval-mode`      | ❌ unsupported（dialog）                                  | ✅ 返回当前审批模式                | 能力扩展           |
| non-interactive 下执行 `/approval-mode yolo` | ❌ unsupported                                            | ✅ 设置模式，返回确认              | 能力扩展           |
| non-interactive 下执行 `/about`              | ❌ 返回 "Command executed successfully."（addItem no-op） | ✅ 返回版本/模型/环境摘要          | Bug fix + 能力扩展 |
| non-interactive 下执行 `/stats`              | ❌ 返回 "Command executed successfully."                  | ✅ 返回 session 统计文本           | Bug fix + 能力扩展 |
| non-interactive 下执行 `/insight`            | ❌ 返回 "Command executed successfully."（生成但无输出）  | ✅ 生成并返回文件路径              | Bug fix + 能力扩展 |
| non-interactive 下执行 `/docs`               | ❌ 返回 "Command executed successfully."                  | ✅ 返回文档 URL                    | Bug fix + 能力扩展 |
| non-interactive 下执行 `/clear`              | ❌ 返回 "Command executed successfully."                  | ✅ 返回上下文边界 message          | Bug fix + 能力扩展 |
| interactive 下执行任意以上命令               | ✅ 原有行为                                               | ✅ 原有行为（零退化）              | 不变               |

---

## 15. 实施顺序

建议按以下顺序实施，每组可独立 commit 和 review：

**Batch 1**（~30min）：A 类 — 只改 `supportedModes`

修改 `exportCommand.ts`（及其子命令），验证测试通过。

**Batch 2**（~45min）：A+ 类 — 少量分支

修改 `languageCommand.ts`，为有副作用的路径添加非交互分支，更新对应测试。（`copyCommand.ts` 和 `restoreCommand.ts` 经讨论保持仅交互。）

**Batch 3**（~45min）：A' 类 — dialog 路径

修改 `modelCommand.ts`、`approvalModeCommand.ts`，为无参数路径添加非交互分支，更新对应测试。

**Batch 4**（~1.5h）：B 类 — 完整分支

修改 `aboutCommand.ts`、`statsCommand.ts`（含子命令）、`docsCommand.ts`。

**Batch 5**（~1h）：B 类特殊 — `insightCommand.ts`、`clearCommand.ts`

这两个命令副作用较多，单独一个 commit，更新对应测试和集成测试。

**Batch 6**（~2h）：Phase 2.2 — prompt command 模型调用打通

修改 `SkillTool`，接入 `getModelInvocableCommands()`，更新 SkillTool 测试。

**Batch 7**（~2h）：Phase 2.3 — mid-input slash 检测

修改 `InputPrompt` 组件，新增补全触发逻辑和 UI 测试。

**Batch 8**（~30min）：全量测试 + 类型检查

运行 `npm run typecheck`、`cd packages/cli && npx vitest run`，修复剩余问题。

---

## 16. 验收 Checklist

**Phase 2.1 命令扩展**

- [ ] A 类：`/export`（及子命令）、`/plan`、`/statusline` 在 non-interactive 和 acp 模式下可正常执行并返回有意义输出
- [ ] A+ 类：`/language`（及子命令）在 non-interactive 下正常执行，设置持久化
- [ ] A+ 类：`/copy` 在 non-interactive/acp 下返回最后 AI 输出文本（不操作剪贴板）
- [ ] A+ 类：`/restore` 无参数时在 non-interactive 下返回 checkpoint 列表；有参数时恢复状态并返回确认 message（不返回 `type: 'tool'`）
- [ ] A' 类：`/model` 无参数时在 non-interactive/acp 下返回当前模型名（不触发 dialog）；`/model --fast <id>` 正常设置
- [ ] A' 类：`/approval-mode` 无参数时在 non-interactive/acp 下返回当前模式（不触发 dialog）；有参数时正常设置
- [ ] B 类：`/about` 在 non-interactive/acp 下返回包含版本号、模型名的纯文本摘要
- [ ] B 类：`/stats`（含子命令）在 non-interactive/acp 下返回纯文本统计数据
- [ ] B 类：`/insight` 在 non-interactive/acp 下生成 insight 文件并返回文件路径（不打开浏览器）
- [ ] B 类：`/docs` 在 non-interactive/acp 下返回文档 URL（不打开浏览器）
- [ ] B 类：`/clear` 在 non-interactive/acp 下返回上下文边界标记 message，`geminiClient.resetChat()` 正常执行
- [ ] 所有 13 个命令在 interactive 模式下行为与重构前完全一致（无退化）
- [ ] TypeScript 编译无错误（`npm run typecheck`）
- [ ] `npm run lint` 无新增错误
- [ ] 所有现有测试通过（`cd packages/cli && npx vitest run`）

**Phase 2.2 模型调用**

- [ ] 模型在对话中可以通过 `SkillTool` 调用 bundled skill、file command（用户/项目）、MCP prompt
- [ ] 模型不可以调用 built-in commands
- [ ] `SkillTool` 的 tool description 包含所有 `modelInvocable: true` 命令的名称和 description

**Phase 2.3 mid-input slash**

- [ ] 在输入框正文中输入 `/` 后触发命令补全菜单（不限行首）
- [ ] 补全菜单展示命令名 + description
- [ ] 补全选中后正确填充到输入框

---

### Design Slash Command Roadmap (`docs/design/slash-command/roadmap.md`)

# Slash Command 重构路线图

## 总体目标

用 Qwen 内部架构风格，交付一个在外部体验上 95% 对齐 Claude Code 的 command 平台，同时修复三模式分裂、命令来源单一、prompt command 无法被模型调用三个核心问题。

---

## 核心设计原则

1. **每个 Phase 可独立 ship**：完成后行为是自洽的，不依赖未来 Phase 才能运行
2. **Phase 1 是纯基础设施**：除修复 MCP_PROMPT 被错误拦截外，不改变任何现有可用命令集
3. **行为变化与架构变化分开**：Phase 1 做架构，Phase 2 做能力扩展
4. **不照搬 Claude Code 内部架构**：但对齐用户可感知的能力面

---

## Phase 1：基础设施重建（纯架构，零行为变化）

### 目标

建立统一的命令元数据模型和跨模式管理机制，为后续所有 Phase 提供底层支撑。

### 功能点

#### 1.1 扩展 `SlashCommand` 元数据模型

在现有 `SlashCommand` 接口上新增以下字段：

**来源字段**

- `source: CommandSource`：命令来源枚举（`builtin-command` / `bundled-skill` / `skill-dir-command` / `plugin-command` / `mcp-prompt` 等）
- `sourceLabel?: string`：展示用的来源标签（如 `"Built-in"` / `"MCP: github-server"`）

**模式能力字段**

- `supportedModes: ExecutionMode[]`：声明在哪些运行模式下可用（`interactive` / `non_interactive` / `acp`）

**执行类型字段**

- `commandType: CommandType`：声明执行类型（`prompt` / `local` / `local-jsx`）

**可见性字段**

- `userInvocable: boolean`：用户是否可通过 slash command 调用（默认 `true`）
- `modelInvocable: boolean`：模型是否可通过 tool call 调用（默认 `false`）

**辅助元数据字段**（为 Phase 3 预留，Phase 1 仅定义，不使用）

- `argumentHint?: string`：参数提示，如 `"<model-id>"` / `"show|list|set"`
- `whenToUse?: string`：何时调用该命令的说明（供模型使用）
- `examples?: string[]`：使用示例

#### 1.2 Loader 填充 source/commandType 字段

每个 Loader 在构建 `SlashCommand` 时必须填充 `source` 和 `commandType`：

| Loader                           | source              | commandType                           |
| -------------------------------- | ------------------- | ------------------------------------- |
| `BuiltinCommandLoader`           | `builtin-command`   | 由各命令声明（`local` / `local-jsx`） |
| `BundledSkillLoader`             | `bundled-skill`     | `prompt`                              |
| `FileCommandLoader`（用户/项目） | `skill-dir-command` | `prompt`                              |
| `FileCommandLoader`（插件）      | `plugin-command`    | `prompt`                              |
| `McpPromptLoader`                | `mcp-prompt`        | `prompt`                              |

#### 1.3 内置命令声明 `supportedModes` 和 `commandType`

为所有 built-in 命令显式声明：

- `commandType`：`local`（无 UI 依赖）或 `local-jsx`（依赖 dialog/React）
- `supportedModes`：`local` 类命令声明 `['interactive', 'non_interactive', 'acp']`；`local-jsx` 类命令声明 `['interactive']`

#### 1.4 用 capability-based 过滤替换硬编码白名单

- 删除 `ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE` 常量
- 删除 `filterCommandsForNonInteractive` 函数
- 新增 `filterCommandsForMode(commands, mode)` 函数，基于 `supportedModes` 字段过滤
- 新增 `getEffectiveSupportedModes(cmd)` 工具函数（考虑 CommandKind 默认策略）
- 修改 `handleSlashCommand` / `getAvailableCommands` 函数签名，移除 `allowedBuiltinCommandNames` 参数

#### 1.5 CommandService 升级为统一 Registry

- 新增 `getCommandsForMode(mode: ExecutionMode)` 方法
- 新增 `getModelInvocableCommands()` 方法（Phase 2/3 使用，Phase 1 提供接口）
- 现有 `getCommands()` 保持不变（interactive 使用）

### 验收标准

- [ ] `SlashCommand` 接口包含所有新字段，TypeScript 编译通过
- [ ] 所有 Loader 填充 `source` 和 `commandType` 字段
- [ ] 所有 built-in 命令声明 `commandType` 和 `supportedModes`
- [ ] `ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE` 已删除，被 capability filter 取代
- [ ] **non-interactive 下可用命令集与重构前完全一致**（现有测试不 break）
- [ ] MCP prompt commands 在 non-interactive/acp 下可正常执行（修复原有错误限制）
- [ ] `CommandService.getCommandsForMode('non_interactive')` 返回正确的命令集
- [ ] 所有现有测试通过

---

## Phase 2：能力扩展（命令整理与 prompt command 模型调用）

### 目标

基于 Phase 1 的元数据基础，扩展三种模式下的命令可用范围，并打通 prompt command 的模型调用通路。

### 功能点

#### 2.1 扩展 non-interactive / acp 可用命令集

**ACP 语义设计原则**

将命令扩展到 ACP/non-interactive 模式前，需遵循以下设计原则：

1. **接收方不同**：ACP 模式下消息的接收方是 IDE（Zed/VS Code 插件），而非终端用户。消息内容以纯文本或 Markdown 格式为宜，不应包含 terminal 专用的 ANSI 样式。
2. **实现策略是增加模式分支，而非替换**：正确做法是在命令的 `action` 内部新增模式判断——interactive 路径保持现有 UI 渲染逻辑不变，non_interactive/acp 路径返回适合机器消费的 `message` 或 `submit_prompt`。两条路径共存于同一个 `action` 函数中。
3. **有状态操作需说明语义**：在单次非交互调用中（如 CLI `-p` 参数），`/model set`、`/language set` 等有状态命令的变更仅在本次 session 内有效，应在命令响应文本中注明。
4. **只读 vs 有副作用**：只读命令（如 `/about`、`/stats`）直接返回当前状态文本；有副作用命令（如 `/model set`、`/language set`）需在响应中确认操作结果。
5. **避免环境相关副作用**：打开浏览器（`/docs`、`/insight`）、操作剪贴板（`/copy`）等依赖图形环境的操作，在 non_interactive/acp 路径下应跳过，改为在响应文本中返回相关 URL 或内容本身。

**待扩展命令总览**

> 注：`btw`、`bug`、`compress`、`context`、`init`、`summary` 已在 Phase 1 中扩展到全模式，不在本阶段列表中。

以下 13 个命令将在 Phase 2 中扩展到 `non_interactive` 和 `acp` 模式：

**A 类：action 已返回 `message` 或 `submit_prompt`，只需扩展 `supportedModes` 并设计 ACP 消息内容**

| 命令          | 返回类型        | ACP/non-interactive 处理要点                       |
| ------------- | --------------- | -------------------------------------------------- |
| `/copy`       | `message`       | ACP 下无剪贴板，改为在响应文本中返回内容本身或提示 |
| `/export`     | `message`       | 返回导出文件的完整路径                             |
| `/plan`       | `submit_prompt` | 无需改动，直接扩展模式                             |
| `/restore`    | `message`       | 返回恢复操作的结果描述                             |
| `/language`   | `message`       | 返回当前语言设置或变更确认文本                     |
| `/statusline` | `submit_prompt` | 无需改动，直接扩展模式                             |

**A' 类：有参数时正常执行，无参数时触发 dialog（需增加无参数路径的 non-interactive 处理）**

| 命令             | 无参数 interactive 行为 | 无参数 non_interactive/acp 行为 |
| ---------------- | ----------------------- | ------------------------------- |
| `/model`         | 打开模型选择 dialog     | 返回当前模型名称及说明文本      |
| `/approval-mode` | 打开审批模式 dialog     | 返回当前审批模式及说明文本      |

**B 类：action 内部使用 `context.ui.addItem()` 渲染 React 组件，需增加模式分支返回纯文本**

| 命令       | interactive 行为          | non_interactive/acp 返回内容                                                        |
| ---------- | ------------------------- | ----------------------------------------------------------------------------------- |
| `/about`   | 渲染版本/配置 React 组件  | 版本号、当前模型、关键配置的纯文本摘要                                              |
| `/stats`   | 渲染 token/费用统计组件   | session 统计数据的纯文本格式                                                        |
| `/insight` | 渲染分析组件 + 打开浏览器 | `non_interactive` 同步生成返回文件路径；`acp` 通过 `stream_messages` 推送进度和结果 |
| `/docs`    | 渲染文档入口 + 打开浏览器 | 返回文档 URL，不打开浏览器                                                          |

**C 类：特殊处理**

| 命令     | interactive 行为                       | non_interactive/acp 行为                                                                            |
| -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `/clear` | 调用 `context.ui.clear()` 清空终端显示 | 返回上下文边界标记 message，内容为 `"Context cleared. Previous messages are no longer in context."` |

#### 2.2 prompt command 模型调用打通

- 在 `CommandService`（或 `CommandRegistry`）中实现 `getModelInvocableCommands()`，返回所有 `modelInvocable: true` 的命令
- 将 `BundledSkillLoader`、`FileCommandLoader`（用户/项目命令）加载的命令标记为 `modelInvocable: true`
- **MCP prompt 不标记为 `modelInvocable`**：MCP prompt 通过独立的 MCP tool call 机制由模型调用，无需经过 `SkillTool` 中转
- 改造 `SkillTool`：从只消费 `SkillManager.listSkills()` 改为同时消费 `CommandService.getModelInvocableCommands()`
- 构建统一的模型可调用命令描述，注入 `SkillTool` 的 description

#### 2.3 mid-input slash command 检测（基础版）

- 在 `InputPrompt` 中检测光标附近的 slash token（不限于行首）
- 检测到 slash token 后通过 inline ghost text 提示最佳匹配命令名（Tab 接受）
- **不**包含 dropdown 补全菜单、argument hints、source badge 等（Phase 3 做）
- ghost text 候选集仅限 `modelInvocable: true` 的命令（skill / file command）

### 验收标准

**2.1 命令扩展**

- [ ] A 类：`/copy`、`/export`、`/plan`、`/restore`、`/language`、`/statusline` 在 non-interactive 和 acp 模式下可正常执行并返回有意义的文本输出
- [ ] A' 类：`/model`、`/approval-mode` 无参数时在 non-interactive/acp 下返回当前状态文本（不触发 dialog）；有参数时执行变更并返回确认文本
- [ ] B 类：`/about`、`/stats`、`/docs` 在 non-interactive/acp 下返回纯文本，`/docs` 不打开浏览器；`/insight` 在 `non_interactive` 下同步生成并返回文件路径 message，在 `acp` 下通过 `stream_messages` 推送进度
- [ ] C 类：`/clear` 在 non-interactive/acp 下返回上下文边界标记 message，不调用 `context.ui.clear()`
- [ ] 所有扩展命令在 interactive 模式下行为与重构前完全一致（无退化）

**2.2 模型调用**

- [ ] 模型在对话中可以通过 `SkillTool` 调用 bundled skill、file command（用户/项目）
- [ ] MCP prompt 不经过 `SkillTool`，通过 MCP tool call 机制由模型原生调用
- [ ] 模型不可以调用 built-in commands（`userInvocable: true`，`modelInvocable: false`）
- [ ] `SkillTool` 的 description 包含所有 `modelInvocable` 命令的描述

**2.3 mid-input slash**

- [ ] mid-input slash：在正文中输入 `/` 后通过 inline ghost text 提示最佳匹配命令（Tab 接受）

---

## Phase 3：体验对齐（补全增强 + Claude Code 命令补齐）

### 目标

在 Phase 1/2 的元数据和命令能力基础上，补齐补全体验，并补充 Claude Code 中存在而 Qwen Code 缺失的命令。

### 功能点

#### 3.1 补全体验增强

**source badge**

- 在补全菜单中展示命令来源标签（`[MCP]` 已有，扩展为 `[Skill]`、`[Custom]` 等）
- 使用 `source` / `sourceLabel` 字段渲染

**argument hint**

- 补全菜单中命令名后展示 `argumentHint`（如 `set <model-id>`）
- `argumentHint` 由 Phase 1 元数据字段提供

**recently used 排序**

- 记录用户最近使用的命令（session 级别，无需持久化）
- 在补全排序中给近期使用的命令加权

**alias 命中高亮**

- 当补全命中 `altNames` 而非主名时，在展示时注明（如 `help (alias: ?)`）

**冲突策略对齐**

- 明确优先级：built-in > bundled/skill-dir > plugin > mcp
- 冲突时将低优先级命令重命名（如 `pluginName.commandName`）

#### 3.2 mid-input slash command 完整版

- 在 Phase 2 基础版上增加 argument hints 和 source badge 展示
- ghost text 提示（输入 `/he` 时显示 `/help` 的淡色提示）
- 有效命令 token 高亮（已完成匹配的 slash command 显示不同颜色）

#### 3.3 Help 目录重构

将 `/help` 从平铺列表改为分组目录：

- **Built-in Commands**（local + local-jsx，注明 mode）
- **Bundled Skills**
- **Custom Commands**（用户/项目 file commands）
- **Plugin Commands**
- **MCP Commands**

每条命令展示：名称、argumentHint、description、source、supportedModes 标记

#### 3.4 ACP available commands 元数据增强

在 `sendAvailableCommandsUpdate()` 中将更多元数据暴露给 ACP 客户端：

- `argumentHint`
- `source`
- `supportedModes`
- `subcommands`（名称列表）
- `modelInvocable`

#### 3.5 Claude Code 缺失命令补齐

确认并回归 Qwen Code 已有的 `/doctor` 命令；`/release-notes` 不纳入本阶段，避免引入无明确产品需求的 built-in 命令表面。

| 命令      | 类型    | 说明                                 |
| --------- | ------- | ------------------------------------ |
| `/doctor` | `local` | 环境自检，输出配置/连接/工具状态诊断 |

> 注：`/commit` 已作为内置 prompt 命令落地（见 #4000）；`/review` 等任务类命令仍以 bundled skill 形式提供，不在此列。

### 验收标准

- [ ] 补全菜单展示 source badge（`[MCP]`、`[Skill]`、`[Custom]`）
- [ ] 补全菜单展示 argumentHint（如 `set <model-id>`）
- [ ] 近期使用的命令在补全列表中优先出现
- [ ] alias 命中时在补全项中注明原名
- [ ] mid-input slash：ghost text 提示正确渲染
- [ ] `/help` 以 Claude Code 风格分 tab 展示，避免命令堆砌，并在命令页展示支持模式标记
- [ ] ACP available commands 包含 `argumentHint`、`source`、`subcommands` 字段
- [ ] `/doctor` 命令可用
- [ ] `/doctor` 在 non-interactive 模式下可执行（返回 `message`）
- [ ] 不新增 `/release-notes`

---

## 各 Phase 依赖关系

```
Phase 1（元数据 + 统一过滤）
    │
    ├──► Phase 2（能力扩展）
    │        │
    │        ├──► slash command 子命令拆分
    │        └──► prompt command 模型调用（需要 getModelInvocableCommands()）
    │
    └──► Phase 3（体验对齐）
             │
             ├──► source badge（需要 Phase 1 source 字段）
             ├──► argument hint（需要 Phase 1 argumentHint 字段）
             └──► Help 分组（需要 Phase 1 source 字段）
```

Phase 2 和 Phase 3 不互相依赖，可以并行推进（或根据优先级调换部分子项）。

---

### Design Tool Use Summary Tool Use Summary Design (`docs/design/tool-use-summary/tool-use-summary-design.md`)

# Tool-Use Summary Design

> Fast-model labels for parallel tool batches — motivation, competitive analysis with Claude Code, architecture, and the append-only-Static rationale that drove the current full-mode render.
>
> User documentation: [Tool-Use Summaries](../../users/features/tool-use-summaries.md).

## 1. Executive Summary

After each tool batch completes, Qwen Code fires a short fast-model call that returns a git-commit-subject-style label summarizing the batch. The label shows as an inline dim `● <label>` line in full mode and replaces the generic `Tool × N` header in compact mode. Generation runs fire-and-forget in parallel with the next turn's API stream, so its ~1s latency is hidden behind main-model streaming.

| Dimension             | Claude Code                                                           | Qwen Code                                                                                  |
| --------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Trigger point         | `query.ts` — after a tool batch finalizes                             | `useGeminiStream.ts` → `handleCompletedTools` — same lifecycle point                       |
| Generation model      | Haiku via `queryHaiku`                                                | Configured `fastModel` via `GeminiClient.generateContent`                                  |
| Subagent behavior     | `!toolUseContext.agentId` — main session only                         | Implicit — subagents run through `agents/runtime/`, not `useGeminiStream`                  |
| Scheduling            | Fire-and-forget, awaited right before the next turn's stream emits    | Fire-and-forget, appended to history when resolved                                         |
| Output shape          | `ToolUseSummaryMessage` yielded into the SDK stream                   | `HistoryItemToolUseSummary` added to UI history + factory exported for future SDK use      |
| Gate                  | `CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES` env, default **off**            | `experimental.emitToolUseSummaries` setting (default **on**) + env override                |
| Primary consumer      | Mobile / SDK clients                                                  | CLI compact mode + full mode, future SDK                                                   |
| Prompt                | Git-commit-subject, past tense, most distinctive noun (verbatim port) | Identical system prompt                                                                    |
| Input truncation      | 300 chars per tool field via `truncateJson`                           | Identical                                                                                  |
| Intent prefix         | First 200 chars of the assistant's last message                       | Identical                                                                                  |
| Prompt caching        | `enablePromptCaching: true` on the Haiku call                         | Not yet wired (forked-agent route available; flagged as future optimization)               |
| Label post-processing | Raw model text                                                        | `cleanSummary` (strips markdown, quotes, error-prefixes; caps at 100 chars, ReDoS-bounded) |
| Session persistence   | Stream-only; each session regenerates                                 | UI history only; `ChatRecordingService` does not persist `tool_use_summary` entries        |

## 2. Claude Code Implementation Analysis

### 2.1 Flow

Claude Code runs the tool loop in `query.ts`. After a tool batch executes and its results are normalized, the generator function forks a Haiku call, keeps the pending promise on `nextPendingToolUseSummary`, and continues with the next turn's API call. The Haiku latency (~1s) overlaps the main model's streaming (5–30s), so the user sees zero added latency. Right before emitting the next turn's content, the generator awaits the pending summary and yields a `tool_use_summary` message into the stream.

```
tool_batch_complete → fork queryHaiku (fire-and-forget)
                          ↓
               next_turn_stream_starts
                          ↓
       ← summary Promise resolves during streaming →
                          ↓
       await pendingToolUseSummary → yield ToolUseSummaryMessage
                          ↓
                continue with next turn
```

### 2.2 Key source files

| Component       | File                                                       | Key logic                                                                               |
| --------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Generator       | `services/toolUseSummary/toolUseSummaryGenerator.ts:45-97` | `generateToolUseSummary({ tools, signal, isNonInteractiveSession, lastAssistantText })` |
| Trigger         | `query.ts:1411-1482`                                       | Guard by `emitToolUseSummaries` gate + no-subagent; fork Haiku; carry promise           |
| Await + emit    | `query.ts:1055-1060`                                       | Await `pendingToolUseSummary` at next-turn boundary, yield message                      |
| Message factory | `utils/messages.ts:5105-5116`                              | `createToolUseSummaryMessage(summary, precedingToolUseIds)`                             |
| Feature gate    | `query/config.ts:23,36-38`                                 | `emitToolUseSummaries: isEnvTruthy(CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES)`                |

### 2.3 Design decisions

1. **Always generate when the gate is on, regardless of compact/detail state.** The summary is a stream-level artifact; the UI decides whether to render it.
2. **Emit as a first-class message type.** `tool_use_summary` sits alongside `user`, `assistant`, `tool_result` in the SDK stream with a `precedingToolUseIds` field for consumers to correlate against the batch.
3. **Subagents are excluded.** `!toolUseContext.agentId` — subagent output is aggregated upstream; individual subagent batches would produce noisy labels that never surface in the primary UI.
4. **Default off.** The env-only gate keeps cost at zero unless a downstream SDK consumer opts in. The CC terminal itself does not render the message.
5. **Input truncation at 300 chars per field.** Covers the dominant cost risk — a single large tool result blowing up the prompt — while keeping enough signal for the label.

## 3. Qwen Code Implementation

### 3.1 Flow

Qwen Code hooks the same lifecycle point (`useGeminiStream.handleCompletedTools`) but renders on both sides of `ui.compactMode` so the feature is useful to CLI users without any SDK plumbing.

```
tool_batch_complete (handleCompletedTools)
           ↓
  config.getEmitToolUseSummaries()?
           ↓
   fork generateToolUseSummary (fire-and-forget)
           ↓
  submitQuery() for next turn (streaming starts)
           ↓
   ← summary Promise resolves during streaming →
           ↓
  addItem({type:'tool_use_summary', summary, precedingToolUseIds})
           ↓
  HistoryItemDisplay renders:
    compactMode=false → ● <label> standalone line
    compactMode=true  → hidden; MainContent lookup injects into CompactToolGroupDisplay header
```

### 3.2 Key source files

| Component           | File                                                                  | Key logic                                                                 |
| ------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Service             | `packages/core/src/services/toolUseSummary.ts`                        | `generateToolUseSummary`, `truncateJson`, `cleanSummary`, message factory |
| Config gate         | `packages/core/src/config/config.ts:getEmitToolUseSummaries`          | Env override → settings → default (true)                                  |
| Trigger             | `packages/cli/src/ui/hooks/useGeminiStream.ts:handleCompletedTools`   | Fires fast-model call, addItem on resolve                                 |
| Full-mode render    | `packages/cli/src/ui/components/HistoryItemDisplay.tsx`               | Renders `● <label>` line when `!compactMode`                              |
| Compact-mode lookup | `packages/cli/src/ui/components/MainContent.tsx`                      | `summaryByCallId` map → `compactLabel` prop to each tool_group            |
| Compact header      | `packages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsx` | Replaces default `Tool × N` with `<Summary> · N tools` when label present |
| Merge handling      | `packages/cli/src/ui/utils/mergeCompactToolGroups.ts`                 | Treats `tool_use_summary` as hidden-in-compact for adjacency              |
| UI type             | `packages/cli/src/ui/types.ts:HistoryItemToolUseSummary`              | `{ type: 'tool_use_summary', summary, precedingToolUseIds }`              |

### 3.3 The `<Static>` append-only constraint

The central architectural decision in this PR is **why the full-mode label is a standalone history item and not a decoration on the tool_group itself**.

Qwen Code renders the transcript via Ink's `<Static>`. Static is append-only: once an item is committed to the terminal buffer, Ink will not repaint that region unless `refreshStatic()` is called to clear and re-render the entire transcript. This is the performance model the CLI depends on — static items don't re-render on every keystroke.

Now consider the fast-model call's timing:

```
T0   tool batch completes, tool_group is pushed to history
T0+ε tool_group renders through <Static> and is committed to the buffer
T0+1s fast-model call resolves with a label
```

At T0+1s, we cannot retroactively add the label to the already-committed tool_group. Two options exist:

1. **Update the tool_group's props + call `refreshStatic()`.** Works, but causes a full transcript repaint on every batch — one of the most expensive UI operations in the app. Visible flash. Unacceptable for a cosmetic label.
2. **Render the summary as its own new history item appended _after_ the tool_group.** Static handles this natively — new items append cleanly, no repaint.

This PR takes option 2 in full mode. The `tool_use_summary` entry is a real history item, rendered as a single dim `● <label>` line by `HistoryItemDisplay`. No `refreshStatic` needed.

Compact mode is different because of `mergeCompactToolGroups`. When consecutive tool*groups merge, `MainContent` already calls `refreshStatic()` — that's an existing codepath, and it re-renders the merged group with the label looked up from history. So compact mode \_does* get the label as a header replacement. To avoid rendering the same label twice (once as the compact header, once as a trailing `● <label>` line), `HistoryItemDisplay` hides the standalone line when `compactMode` is true.

```
Full mode              Compact mode (with merge)
───────────            ─────────────────────────
[tool_group]           [merged tool_group — header replaced via lookup]
● <label>              (● <label> line is hidden)
```

### 3.4 Gate semantics

Three layers, resolved in order of precedence:

1. `QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0|1|true|false` — env override, highest priority.
2. `experimental.emitToolUseSummaries` in `settings.json` — default `true`.
3. Implicit skip — if `config.getFastModel()` returns `undefined`, generation is skipped regardless of the gate. No error, no user-visible change.

### 3.5 Output cleaning

`cleanSummary` runs on every model response before it is added to history:

1. Take the first line only (drops model reasoning preambles).
2. Strip bullet prefixes (`-`, `*`, `•`) — models sometimes return the label as a list item.
3. Strip surrounding quotes/backticks via a bounded `{1,10}` regex (CodeQL-safe; no real label has more than a handful of wrapping quotes).
4. Strip prefix labels (`Label:`, `Summary:`, `Result:`, `Output:`) that some models prepend.
5. Reject error-message shapes (`API error: ...`, `Error: ...`, `I cannot ...`, `I can't ...`, `Unable to ...`) — returns empty string so no history item is added.
6. Hard-cap length at 100 characters (mobile UI truncates around 30; the slack covers CJK phrases).

### 3.6 Telemetry

The summary generation call sets `promptId: 'tool_use_summary_generation'` so its token usage is accounted separately in `/stats`. This lets users see the exact incremental cost of the feature without conflating it with prompt suggestions or the main session's usage.

## 4. Deviations from Claude Code (and why)

| Deviation                                                                | Why                                                                                                                                                                                     |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Settings layer in addition to env gate                                   | Qwen Code renders the label in the CLI; users need a persistent switch, not a per-shell env export.                                                                                     |
| Default **on** instead of off                                            | Label is immediately user-visible in both display modes; users configuring `fastModel` are opting into fast-model features already.                                                     |
| Dedicated `cleanSummary` post-processing                                 | Qwen Code supports more heterogeneous providers than CC; some models prepend `Label:` or wrap in quotes. Normalizing at the boundary keeps the UI consistent.                           |
| Stores `HistoryItemToolUseSummary` rather than emitting a stream message | CLI-first implementation; the SDK-stream route is a future PR. The `ToolUseSummaryMessage` factory is already exported for that work.                                                   |
| Prompt caching not yet wired                                             | The fast model is often the same as the main model for users who haven't configured a separate one. Adding cache sharing requires routing via `forkedAgent.ts`; tracked as a follow-up. |
| Dual render paths (full-mode inline + compact-mode header)               | Qwen Code's default is `ui.compactMode: false`; without the inline full-mode render, the feature would be invisible to most users.                                                      |

## 5. Known limitations

- **No session persistence.** `tool_use_summary` is not written to the chat recording JSONL. Resuming a session loses labels; tool groups render with the generic header as a fallback. Low-priority: labels regenerate naturally as the user continues the session.
- **No SDK stream emission yet.** The message factory is exported, but the CLI does not yet feed `tool_use_summary` into the SDK bridge. Follow-up PR.
- **No prompt caching.** Each batch incurs a fresh input-token cost. Negligible in absolute terms (~300 tokens) but measurable if you run dozens of batches per turn.
- **Summary for merged compact groups picks the first contributing batch's label.** If a user fires ten dissimilar batches back-to-back (tight loop, not typical), the merged compact header will show only the leading batch's intent. Trade-off accepted: fanning out per-batch labels in a merged view is visually noisier than taking the first.
- **Fast model required.** Without a configured `fastModel`, generation is skipped. Falling back to the main model is deliberately disallowed to keep the cost profile bounded.

## 6. Future work

1. Wire `ToolUseSummaryMessage` into the SDK bridge so the existing factory gets used downstream.
2. Route generation via `forkedAgent.ts` with `enablePromptCaching` so repeated tool-name prefixes hit provider caches.
3. Optional: persist `tool_use_summary` entries to `ChatRecordingService` and replay on session resume.
4. Optional: per-tool-name label shortcuts (e.g., always `Read <filename>` for a single `read_file` call) as a pre-LLM fast path.

---

### Developers Development Deployment (`docs/developers/development/deployment.md`)

# Qwen Code Execution and Deployment

This document describes how to run Qwen Code and explains the deployment architecture that Qwen Code uses.

## Running Qwen Code

There are several ways to run Qwen Code. The option you choose depends on how you intend to use it.

---

### 1. Standard installation (Recommended for typical users)

This is the recommended way for end-users to install Qwen Code. It involves downloading the Qwen Code package from the NPM registry.

- **Global install:**

  ```bash
  npm install @qwen-code/qwen-code
  ```

  Then, run the CLI from anywhere:

  ```bash
  qwen
  ```

- **NPX execution:**

  ```bash
  # Execute the latest version from NPM without a global install
  npx @qwen-code/qwen-code
  ```

---

### 2. Running in a sandbox (Docker/Podman)

For security and isolation, Qwen Code can be run inside a container. This is the default way that the CLI executes tools that might have side effects.

- **Directly from the Registry:**
  You can run the published sandbox image directly. This is useful for environments where you only have Docker and want to run the CLI.
  ```bash
  # Run the published sandbox image
  docker run --rm -it ghcr.io/qwenlm/qwen-code:0.0.11
  ```
- **Using the `--sandbox` flag:**
  If you have Qwen Code installed locally (using the standard installation described above), you can instruct it to run inside the sandbox container.
  ```bash
  qwen --sandbox -y -p "your prompt here"
  ```

---

### 3. Running from source (Recommended for Qwen Code contributors)

Contributors to the project will want to run the CLI directly from the source code.

- **Development Mode:**
  This method provides hot-reloading and is useful for active development.
  ```bash
  # From the root of the repository
  npm run start
  ```
- **Production-like mode (Linked package):**
  This method simulates a global installation by linking your local package. It's useful for testing a local build in a production workflow.

  ```bash
  # Link the local cli package to your global node_modules
  npm link packages/cli

  # Now you can run your local version using the `qwen` command
  qwen
  ```

---

### 4. Running the latest Qwen Code commit from GitHub

You can run the most recently committed version of Qwen Code directly from the GitHub repository. This is useful for testing features still in development.

```bash
# Execute the CLI directly from the main branch on GitHub
npx https://github.com/QwenLM/qwen-code
```

## Deployment architecture

The execution methods described above are made possible by the following architectural components and processes:

**NPM packages**

Qwen Code project is a monorepo that publishes core packages to the NPM registry:

- `@qwen-code/qwen-code-core`: The backend, handling logic and tool execution.
- `@qwen-code/qwen-code`: The user-facing frontend.

These packages are used when performing the standard installation and when running Qwen Code from the source.

**Build and packaging processes**

There are two distinct build processes used, depending on the distribution channel:

- **NPM publication:** For publishing to the NPM registry, the TypeScript source code in `@qwen-code/qwen-code-core` and `@qwen-code/qwen-code` is transpiled into standard JavaScript using the TypeScript Compiler (`tsc`). The resulting `dist/` directory is what gets published in the NPM package. This is a standard approach for TypeScript libraries.

- **GitHub `npx` execution:** When running the latest version of Qwen Code directly from GitHub, a different process is triggered by the `prepare` script in `package.json`. This script uses `esbuild` to bundle the entire application and its dependencies into a single, self-contained JavaScript file. This bundle is created on-the-fly on the user's machine and is not checked into the repository.

**Docker sandbox image**

The Docker-based execution method is supported by the `qwen-code-sandbox` container image. This image is published to a container registry and contains a pre-installed, global version of Qwen Code.

## Release process

The release process is automated through GitHub Actions. The release workflow performs the following actions:

1.  Build the NPM packages using `tsc`.
2.  Publish the NPM packages to the artifact registry.
3.  Create GitHub releases with bundled assets.

**OpenTUI preview flavor**

Standalone archives ship in the classic Node.js flavor by default. Setting the `OPENTUI_PREVIEW_RELEASE_ENABLED` repository variable to `true` additionally builds the bun/OpenTUI preview flavor, whose archives carry an `-opentui-preview` suffix and whose launcher defaults `QWEN_TUI_RENDERER` to `opentui`. Leaving the variable unset keeps that flavor off.

The variable decides only what a new release builds. The workflow that mirrors a release to Aliyun OSS does not read it: that workflow can be re-dispatched for any tag, and a tag cut before the flavor existed has no preview archives to verify, so it derives the flavor from the archives the release actually shipped.

---

### Developers Development Integration Tests (`docs/developers/development/integration-tests.md`)

# Integration Tests

This document provides information about the integration testing framework used in this project.

## Overview

The integration tests are designed to validate the end-to-end functionality of Qwen Code. They execute the built binary in a controlled environment and verify that it behaves as expected when interacting with the file system.

These tests are located in the `integration-tests` directory and are run using a custom test runner.

## Running the tests

The integration tests are not run as part of the default `npm run test` command. They must be run explicitly using the `npm run test:integration:all` script.

The integration tests can also be run using the following shortcut:

```bash
npm run test:e2e
```

## Running a specific set of tests

To run a subset of test files, you can use `npm run <integration test command> <file_name1> ....` where &lt;integration test command&gt; is either `test:e2e` or `test:integration*` and `<file_name>` is any of the `.test.ts` files in the `integration-tests/` directory. For example, the following command runs `list_directory.test.ts` and `write_file.test.ts`:

```bash
npm run test:e2e list_directory write_file
```

### Running a single test by name

To run a single test by its name, use the `--test-name-pattern` flag:

```bash
npm run test:e2e -- --test-name-pattern "reads a file"
```

### Running all tests

To run the entire suite of integration tests, use the following command:

```bash
npm run test:integration:all
```

### Sandbox matrix

The `all` command will run tests for `no sandboxing`, `docker` and `podman`.
Each individual type can be run using the following commands:

```bash
npm run test:integration:sandbox:none
```

```bash
npm run test:integration:sandbox:docker
```

```bash
npm run test:integration:sandbox:podman
```

## Diagnostics

The integration test runner provides several options for diagnostics to help track down test failures.

### Keeping test output

You can preserve the temporary files created during a test run for inspection. This is useful for debugging issues with file system operations.

To keep the test output set the `KEEP_OUTPUT` environment variable to `true`.

```bash
KEEP_OUTPUT=true npm run test:integration:sandbox:none
```

When output is kept, the test runner will print the path to the unique directory for the test run.

### Verbose output

For more detailed debugging, set the `VERBOSE` environment variable to `true`.

```bash
VERBOSE=true npm run test:integration:sandbox:none
```

When using `VERBOSE=true` and `KEEP_OUTPUT=true` in the same command, the output is streamed to the console and also saved to a log file within the test's temporary directory.

The verbose output is formatted to clearly identify the source of the logs:

```
--- TEST: <log dir>:<test-name> ---
... output from the qwen command ...
--- END TEST: <log dir>:<test-name> ---
```

## Linting and formatting

To ensure code quality and consistency, the integration test files are linted as part of the main build process. You can also manually run the linter and auto-fixer.

### Running the linter

To check for linting errors, run the following command:

```bash
npm run lint
```

You can include the `:fix` flag in the command to automatically fix any fixable linting errors:

```bash
npm run lint:fix
```

## Directory structure

The integration tests create a unique directory for each test run inside the `.integration-tests` directory. Within this directory, a subdirectory is created for each test file, and within that, a subdirectory is created for each individual test case.

This structure makes it easy to locate the artifacts for a specific test run, file, or case.

```
.integration-tests/
└── <run-id>/
    └── <test-file-name>.test.ts/
        └── <test-case-name>/
            ├── output.log
            └── ...other test artifacts...
```

## Continuous integration

To ensure the integration tests are always run, a GitHub Actions workflow is defined in `.github/workflows/e2e.yml`. This workflow automatically runs the integrations tests for pull requests against the `main` branch, or when a pull request is added to a merge queue.

The workflow runs the tests in different sandboxing environments to ensure Qwen Code is tested across each:

- `sandbox:none`: Runs the tests without any sandboxing.
- `sandbox:docker`: Runs the tests in a Docker container.
- `sandbox:podman`: Runs the tests in a Podman container.

---

### Developers Development Issue And Pr Automation (`docs/developers/development/issue-and-pr-automation.md`)

# Automation and Triage Processes

This document provides a detailed overview of the automated processes we use to manage and triage issues and pull requests. Our goal is to provide prompt feedback and ensure that contributions are reviewed and integrated efficiently. Understanding this automation will help you as a contributor know what to expect and how to best interact with our repository bots.

## Guiding Principle: Issues and Pull Requests

First and foremost, almost every Pull Request (PR) should be linked to a corresponding Issue. The issue describes the "what" and the "why" (the bug or feature), while the PR is the "how" (the implementation). This separation helps us track work, prioritize features, and maintain clear historical context. Our automation is built around this principle.

---

## Detailed Automation Workflows

Here is a breakdown of the specific automation workflows that run in our repository.

### 1. When you open an Issue: `Qwen Triage`

This is the first bot you will interact with when you create an issue. Its job is to perform an initial analysis and apply the correct labels.

- **Workflow File**: `.github/workflows/qwen-triage.yml`
- **When it runs**: Immediately after an issue is created, edited, or reopened, or when a maintainer manually requests triage.
- **What it does**:
  - It uses a Qwen model to analyze the issue's title and body against a detailed set of guidelines.
  - **Applies one `area/*` label**: Categorizes the issue into a functional area of the project (e.g., `area/ux`, `area/models`, `area/platform`).
  - **Applies one `kind/*` label**: Identifies the type of issue (e.g., `kind/bug`, `kind/enhancement`, `kind/question`).
  - **Applies one `priority/*` label**: Assigns a priority from P0 (critical) to P3 (low) based on the described impact.
  - **May apply `status/need-information`**: If the issue lacks critical details (like logs or reproduction steps), it will be flagged for more information.
  - **May apply `status/need-retesting`**: If the issue references a CLI version that is more than six versions old, it will be flagged for retesting on a current version.
- **What you should do**:
  - Fill out the issue template as completely as possible. The more detail you provide, the more accurate the triage will be.
  - If the `status/need-information` label is added, please provide the requested details in a comment.
  - Maintainers can comment `@qwen-code /triage` to run triage again.

### 2. When you open a Pull Request: `Continuous Integration (CI)`

This workflow ensures that all changes meet our quality standards before they can be merged.

- **Workflow File**: `.github/workflows/ci.yml`
- **When it runs**: On every push to a pull request.
- **What it does**:
  - **Lint**: Checks that your code adheres to our project's formatting and style rules.
  - **Test**: Runs our full suite of automated tests across macOS, Windows, and Linux, and on multiple Node.js versions. This is the most time-consuming part of the CI process.
  - **Post Coverage Comment**: After all tests have successfully passed, a bot will post a comment on your PR. This comment provides a summary of how well your changes are covered by tests.
- **What you should do**:
  - Ensure all CI checks pass. A green checkmark ✅ will appear next to your commit when everything is successful.
  - If a check fails (a red "X" ❌), click the "Details" link next to the failed check to view the logs, identify the problem, and push a fix.

### 3. Release Automation

This workflow handles the process of packaging and publishing new versions of Qwen Code.

- **Workflow File**: `.github/workflows/release.yml`
- **When it runs**: On a daily schedule for "nightly" releases, and manually for official patch/minor releases.
- **What it does**:
  - Automatically builds the project, bumps the version numbers, and publishes the packages to npm.
  - Creates a corresponding release on GitHub with generated release notes.
- **What you should do**:
  - As a contributor, you don't need to do anything for this process. You can be confident that once your PR is merged into the `main` branch, your changes will be included in the very next nightly release.

We hope this detailed overview is helpful. If you have any questions about our automation or processes, please don't hesitate to ask!

---

### Developers Development Npm (`docs/developers/development/npm.md`)

# Package Overview

This monorepo contains two main packages: `@qwen-code/qwen-code` and `@qwen-code/qwen-code-core`.

## `@qwen-code/qwen-code`

This is the main package for Qwen Code. It is responsible for the user interface, command parsing, and all other user-facing functionality.

When this package is published, it is bundled into a single executable file. This bundle includes all of the package's dependencies, including `@qwen-code/qwen-code-core`. This means that whether a user installs the package with `npm install @qwen-code/qwen-code` or runs it directly with `npx @qwen-code/qwen-code`, they are using this single, self-contained executable.

## `@qwen-code/qwen-code-core`

This package contains the core logic for the CLI. It is responsible for making API requests to configured providers, handling authentication, and managing the local cache.

This package is not bundled. When it is published, it is published as a standard Node.js package with its own dependencies. This allows it to be used as a standalone package in other projects, if needed. All transpiled js code in the `dist` folder is included in the package.

# Release Process

This project follows a structured release process to ensure that all packages are versioned and published correctly. The process is designed to be as automated as possible.

## How To Release

Releases are managed through the [release.yml](https://github.com/QwenLM/qwen-code/actions/workflows/release.yml) GitHub Actions workflow. To perform a manual release for a patch or hotfix:

1.  Navigate to the **Actions** tab of the repository.
2.  Select the **Release** workflow from the list.
3.  Click the **Run workflow** dropdown button.
4.  Fill in the required inputs:
    - **Version**: The exact version to release (e.g., `v0.2.1`).
    - **Ref**: The branch or commit SHA to release from (defaults to `main`).
    - **Dry Run**: Leave as `true` to test the workflow without publishing, or set to `false` to perform a live release.
5.  Click **Run workflow**.

## Release Types

The project supports multiple types of releases:

### Stable Releases

Regular stable releases for production use.

### Preview Releases

Weekly preview releases every Tuesday at 17:00 UTC for early access to upcoming features.

### Nightly Releases

Daily nightly releases at 21:00 UTC for bleeding-edge development testing.

## Automated Release Schedule

- **Nightly**: Every day at 21:00 UTC
- **Preview**: Every Tuesday at 17:00 UTC
- **Stable**: Manual releases triggered by maintainers

### How to Use Different Release Types

To install the latest version of each type:

```bash
# Stable (default)
npm install @qwen-code/qwen-code

# Preview
npm install @qwen-code/qwen-code@preview

# Nightly
npm install @qwen-code/qwen-code@nightly
```

### Release Process Details

Every scheduled or manual release follows these steps:

1.  Checks out the specified code (latest from `main` branch or specific commit).
2.  Installs all dependencies.
3.  Runs the full suite of `preflight` checks and integration tests.
4.  If all tests succeed, it calculates the appropriate version number based on release type.
5.  Builds and publishes the packages to npm with the appropriate dist-tag.
6.  Creates a GitHub Release for the version.

### Failure Handling

If any step in the release workflow fails, it will automatically create a new issue in the repository with the labels `bug` and a type-specific failure label (e.g., `nightly-failure`, `preview-failure`). The issue will contain a link to the failed workflow run for easy debugging.

## Release Validation

After pushing a new release smoke testing should be performed to ensure that the packages are working as expected. This can be done by installing the packages locally and running a set of tests to ensure that they are functioning correctly.

- `npx -y @qwen-code/qwen-code@latest --version` to validate the push worked as expected if you were not doing a rc or dev tag
- `npx -y @qwen-code/qwen-code@<release tag> --version` to validate the tag pushed appropriately
- _This is destructive locally_ `npm uninstall @qwen-code/qwen-code && npm uninstall -g @qwen-code/qwen-code && npm cache clean --force &&  npm install @qwen-code/qwen-code@<version>`
- Smoke testing a basic run through of exercising a few llm commands and tools is recommended to ensure that the packages are working as expected. We'll codify this more in the future.

## When to merge the version change, or not?

The above pattern for creating patch or hotfix releases from current or older commits leaves the repository in the following state:

1.  The Tag (`vX.Y.Z-patch.1`): This tag correctly points to the original commit on main
    that contains the stable code you intended to release. This is crucial. Anyone checking
    out this tag gets the exact code that was published.
2.  The Branch (`release-vX.Y.Z-patch.1`): This branch contains one new commit on top of the
    tagged commit. That new commit only contains the version number change in package.json
    (and the other workspace manifests).

This separation is good. It keeps your main branch history clean of release-specific
version bumps until you decide to merge them.

This is the critical decision, and it depends entirely on the nature of the release.

### Merge Back for Stable Patches and Hotfixes

You almost always want to merge the `release-<tag>` branch back into `main` for any
stable patch or hotfix release.

- Why? The primary reason is to update the version in main's package.json. If you release
  v1.2.1 from an older commit but never merge the version bump back, your main branch's
  package.json will still say "version": "1.2.0". The next developer who starts work for
  the next feature release (v1.3.0) will be branching from a codebase that has an
  incorrect, older version number. This leads to confusion and requires manual version
  bumping later.
- The Process: After the release-v1.2.1 branch is created and the package is successfully
  published, you should open a pull request to merge release-v1.2.1 into main. This PR
  will contain just one commit: "chore: bump version to v1.2.1". It's a clean, simple
  integration that keeps your main branch in sync with the latest released version.

### Do NOT Merge Back for Pre-Releases (RC, Beta, Dev)

You typically do not merge release branches for pre-releases back into `main`.

- Why? Pre-release versions (e.g., v1.3.0-rc.1, v1.3.0-rc.2) are, by definition, not
  stable and are temporary. You don't want to pollute your main branch's history with a
  series of version bumps for release candidates. The package.json in main should reflect
  the latest stable release version, not an RC.
- The Process: The release-v1.3.0-rc.1 branch is created, the npm publish --tag rc happens,
  and then... the branch has served its purpose. You can simply delete it. The code for
  the RC is already on main (or a feature branch), so no functional code is lost. The
  release branch was just a temporary vehicle for the version number.

## Local Testing and Validation: Changes to the Packaging and Publishing Process

If you need to test the release process without actually publishing to NPM or creating a public GitHub release, you can trigger the workflow manually from the GitHub UI.

1.  Go to the [Actions tab](https://github.com/QwenLM/qwen-code/actions/workflows/release.yml) of the repository.
2.  Click on the "Run workflow" dropdown.
3.  Leave the `dry_run` option checked (`true`).
4.  Click the "Run workflow" button.

This will run the entire release process but will skip the `npm publish` and `gh release create` steps. You can inspect the workflow logs to ensure everything is working as expected.

It is crucial to test any changes to the packaging and publishing process locally before committing them. This ensures that the packages will be published correctly and that they will work as expected when installed by a user.

To validate your changes, you can perform a dry run of the publishing process. This will simulate the publishing process without actually publishing the packages to the npm registry.

```bash
npm_package_version=9.9.9 SANDBOX_IMAGE_REGISTRY="registry" SANDBOX_IMAGE_NAME="thename" npm run publish:npm --dry-run
```

This command will do the following:

1.  Build all the packages.
2.  Run all the prepublish scripts.
3.  Create the package tarballs that would be published to npm.
4.  Print a summary of the packages that would be published.

You can then inspect the generated tarballs to ensure that they contain the correct files and that the `package.json` files have been updated correctly. The tarballs will be created in the root of each package's directory (e.g., `packages/cli/qwen-code-0.1.6.tgz`).

By performing a dry run, you can be confident that your changes to the packaging process are correct and that the packages will be published successfully.

## Release Deep Dive

The main goal of the release process is to take the source code from the packages/ directory, build it, and assemble a
clean, self-contained package in a temporary `dist` directory at the root of the project. This `dist` directory is what
actually gets published to NPM.

Here are the key stages:

Stage 1: Pre-Release Sanity Checks and Versioning

- What happens: Before any files are moved, the process ensures the project is in a good state. This involves running tests,
  linting, and type-checking (npm run preflight). The version number in the root package.json and packages/cli/package.json
  is updated to the new release version.
- Why: This guarantees that only high-quality, working code is released. Versioning is the first step to signify a new
  release.

Stage 2: Building the Source Code

- What happens: The TypeScript source code in packages/core/src and packages/cli/src is compiled into JavaScript.
- File movement:
  - packages/core/src/\*_/_.ts -> compiled to -> packages/core/dist/
  - packages/cli/src/\*_/_.ts -> compiled to -> packages/cli/dist/
- Why: The TypeScript code written during development needs to be converted into plain JavaScript that can be run by
  Node.js. The core package is built first as the cli package depends on it.

Stage 3: Bundling and Assembling the Final Publishable Package

This is the most critical stage where files are moved and transformed into their final state for publishing. The process uses modern bundling techniques to create the final package.

1.  Bundle Creation:
    - What happens: The prepare-package.js script creates a clean distribution package in the `dist` directory.
    - Key transformations:
      - Copies README.md and LICENSE to dist/
      - Copies locales folder for internationalization
      - Creates a clean package.json for distribution with only necessary dependencies
      - Keeps distribution dependencies minimal (no bundled runtime deps)
      - Maintains optional dependencies for node-pty

2.  The JavaScript Bundle is Created:
    - What happens: The built JavaScript from both packages/core/dist and packages/cli/dist are bundled into a single,
      executable JavaScript file using esbuild.
    - File location: dist/cli.js
    - Why: This creates a single, optimized file that contains all the necessary application code. It simplifies the package
      by removing the need for complex dependency resolution at install time.

3.  Static and Supporting Files are Copied:
    - What happens: Essential files that are not part of the source code but are required for the package to work correctly
      or be well-described are copied into the `dist` directory.
    - File movement:
      - README.md -> dist/README.md
      - LICENSE -> dist/LICENSE
      - locales/ -> dist/locales/
      - Vendor files -> dist/vendor/
    - Why:
      - The README.md and LICENSE are standard files that should be included in any NPM package.
      - Locales support internationalization features
      - Vendor files contain necessary runtime dependencies

Stage 4: Publishing to NPM

- What happens: The npm publish command is run from inside the root `dist` directory.
- Why: By running npm publish from within the `dist` directory, only the files we carefully assembled in Stage 3 are uploaded
  to the NPM registry. This prevents any source code, test files, or development configurations from being accidentally
  published, resulting in a clean and minimal package for users.

This process ensures that the final published artifact is a purpose-built, clean, and efficient representation of the
project, rather than a direct copy of the development workspace.

## NPM Workspaces

This project uses [NPM Workspaces](https://docs.npmjs.com/cli/v10/using-npm/workspaces) to manage the packages within this monorepo. This simplifies development by allowing us to manage dependencies and run scripts across multiple packages from the root of the project.

### How it Works

The root `package.json` file defines the workspaces for this project:

```json
{
  "workspaces": ["packages/*"]
}
```

This tells NPM that any folder inside the `packages` directory is a separate package that should be managed as part of the workspace.

### Benefits of Workspaces

- **Simplified Dependency Management**: Running `corepack pnpm install --frozen-lockfile` from the root of the project installs the dependencies of every package in the workspace and links the packages together, so you don't need to install in each package's directory.
- **Automatic Linking**: Packages within the workspace can depend on each other. When you install from the root, pnpm creates symlinks between the packages. This means that when you make changes to one package, the changes are immediately available to other packages that depend on it.
- **Simplified Script Execution**: You can run scripts in any package from the root of the project using the `--workspace` flag. For example, to run the `build` script in the `cli` package, you can run `npm run build --workspace @qwen-code/qwen-code`.

---

### Developers Development Telemetry (`docs/developers/development/telemetry.md`)

# Observability with OpenTelemetry

Learn how to enable and setup OpenTelemetry for Qwen Code.

- [Observability with OpenTelemetry](#observability-with-opentelemetry)
  - [Key Benefits](#key-benefits)
  - [OpenTelemetry Integration](#opentelemetry-integration)
  - [Configuration](#configuration)
  - [Aliyun Telemetry](#aliyun-telemetry)
    - [Manual OTLP Export](#manual-otlp-export)
  - [Local Telemetry](#local-telemetry)
    - [File-based Output (Recommended)](#file-based-output-recommended)
    - [Collector-Based Export (Advanced)](#collector-based-export-advanced)
  - [Logs and Metrics](#logs-and-metrics)
    - [Logs](#logs)
    - [Metrics](#metrics)
    - [Daemon Metrics](#daemon-metrics)
    - [Spans](#spans)
    - [Resource Metrics](#resource-metrics)
    - [Performance Monitoring (Reserved)](#performance-monitoring-reserved)

## Migration Notes

- `tool_output_truncated` was renamed to `qwen-code.tool_output_truncated` for namespace consistency — downstream consumers filtering on the old name should update their queries.

- The `tool.call.latency` histogram documentation previously listed a `decision` attribute — this was never set on the histogram (only `function_name` is recorded). The `tool.call.count` counter continues to include `decision`.

- The `qwen-code.file_operation` log event and `file.operation.count` metric documentation previously listed diff-stat attributes (`model_added_lines`, `model_removed_lines`, `user_added_lines`, `user_removed_lines`) — these were never set on either. Diff-stat data is available via the `tool_call` log event's `metadata` attribute.

## Key Benefits

- **🔍 Usage Analytics**: Understand interaction patterns and feature adoption
  across your team
- **⚡ Performance Monitoring**: Track response times, token consumption, and
  resource utilization
- **🐛 Real-time Debugging**: Identify bottlenecks, failures, and error patterns
  as they occur
- **📊 Workflow Optimization**: Make informed decisions to improve
  configurations and processes
- **🏢 Enterprise Governance**: Monitor usage across teams, track costs, ensure
  compliance, and integrate with existing monitoring infrastructure

## OpenTelemetry Integration

Built on **[OpenTelemetry]** — the vendor-neutral, industry-standard
observability framework — Qwen Code's observability system provides:

- **Universal Compatibility**: Export to any OpenTelemetry backend (Aliyun,
  Jaeger, Prometheus, Datadog, etc.)
- **Standardized Data**: Use consistent formats and collection methods across
  your toolchain
- **Future-Proof Integration**: Connect with existing and future observability
  infrastructure
- **No Vendor Lock-in**: Switch between backends without changing your
  instrumentation

[OpenTelemetry]: https://opentelemetry.io/
[aliyun-opentelemetry-overview]: https://www.alibabacloud.com/help/en/arms/tracing-analysis/product-overview/what-is-tracing-analysis
[aliyun-opentelemetry-get-started]: https://www.alibabacloud.com/help/en/arms/tracing-analysis/before-you-begin
[aliyun-opentelemetry-console-cn]: https://trace.console.aliyun.com
[aliyun-opentelemetry-console-cn-legacy]: https://tracing.console.aliyun.com
[aliyun-opentelemetry-console-intl]: https://arms.console.alibabacloud.com

## Configuration

All telemetry behavior is controlled through your `.qwen/settings.json` file.
These settings can be overridden by environment variables or CLI flags.

| Setting                           | Environment Variable                                 | CLI Flag                                                 | Description                                                                                                                            | Values            | Default                 |
| --------------------------------- | ---------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ----------------------- |
| `enabled`                         | `QWEN_TELEMETRY_ENABLED`                             | `--telemetry` / `--no-telemetry`                         | Enable or disable telemetry                                                                                                            | `true`/`false`    | `false`                 |
| `target`                          | `QWEN_TELEMETRY_TARGET`                              | `--telemetry-target <local\|gcp>` _(deprecated)_         | Informational destination label; does not control exporter routing — set `otlpEndpoint` or `outfile` to configure where data is sent   | `"gcp"`/`"local"` | `"local"`               |
| `otlpEndpoint`                    | `QWEN_TELEMETRY_OTLP_ENDPOINT`                       | `--telemetry-otlp-endpoint <URL>`                        | OTLP collector endpoint                                                                                                                | URL string        | `http://localhost:4317` |
| `otlpProtocol`                    | `QWEN_TELEMETRY_OTLP_PROTOCOL`                       | `--telemetry-otlp-protocol <grpc\|http>`                 | OTLP exporter protocol: `grpc` = gRPC/protobuf; `http` = HTTP/JSON                                                                     | `"grpc"`/`"http"` | `"grpc"`                |
| `otlpTracesEndpoint`              | `QWEN_TELEMETRY_OTLP_TRACES_ENDPOINT`                | -                                                        | Per-signal endpoint override for traces (HTTP only)                                                                                    | URL string        | -                       |
| `otlpLogsEndpoint`                | `QWEN_TELEMETRY_OTLP_LOGS_ENDPOINT`                  | -                                                        | Per-signal endpoint override for logs (HTTP only)                                                                                      | URL string        | -                       |
| `otlpMetricsEndpoint`             | `QWEN_TELEMETRY_OTLP_METRICS_ENDPOINT`               | -                                                        | Per-signal endpoint override for metrics (HTTP only)                                                                                   | URL string        | -                       |
| `outfile`                         | `QWEN_TELEMETRY_OUTFILE`                             | `--telemetry-outfile <path>`                             | Save telemetry to file (overrides OTLP export)                                                                                         | file path         | -                       |
| `logPrompts`                      | `QWEN_TELEMETRY_LOG_PROMPTS`                         | `--telemetry-log-prompts` / `--no-telemetry-log-prompts` | Include user prompt content and API request/response text in telemetry logs                                                            | `true`/`false`    | `true`                  |
| `userId`                          | `QWEN_TELEMETRY_USER_ID`                             | -                                                        | Stable end-user identifier written to GenAI spans as the ARMS extension `gen_ai.user.id`; prefer a pseudonymous value                  | string            | -                       |
| `includeSensitiveSpanAttributes`  | `QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES`   | -                                                        | Include standard GenAI messages, instructions, tool definitions, tool arguments, and successful tool results as native span attributes | `true`/`false`    | `false`                 |
| `sensitiveSpanAttributeMaxLength` | `QWEN_TELEMETRY_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH` | -                                                        | Maximum compact JSON string length for each sensitive native span attribute. Set lower if your backend rejects large attributes.       | `1..104857600`    | `1048576`               |
| `resourceAttributes`              | `OTEL_RESOURCE_ATTRIBUTES` (+ `OTEL_SERVICE_NAME`)   | -                                                        | Static resource attributes attached to every exported span / log / metric. See [Resource attributes](#resource-attributes) below.      | `key=value,…`     | `{}`                    |
| `metrics.includeSessionId`        | `QWEN_TELEMETRY_METRICS_INCLUDE_SESSION_ID`          | -                                                        | Include `session.id` on metric data points. **Disabled by default** to protect metric backends from time-series fan-out.               | `true`/`false`    | `false`                 |

**Note on boolean environment variables:** For the boolean settings (`enabled`,
`logPrompts`, `includeSensitiveSpanAttributes`), setting the
corresponding environment variable to `true` or `1` will enable the feature. Any
other value will disable it.

**Note on integer environment variables:** `QWEN_TELEMETRY_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH`
must be a positive integer when set. Invalid values fail telemetry configuration
resolution instead of silently falling back.

`gen_ai.tool.description` is non-sensitive static registry metadata and is
emitted independently of `includeSensitiveSpanAttributes`. This includes
descriptions supplied by MCP servers and other workspace tool providers. The
value is limited to 4096 UTF-16 code units and never includes dynamic invocation
details.

**Sensitive span attributes:** When `includeSensitiveSpanAttributes` is enabled,
two things happen:

1. **Native span attributes** carry standard OpenTelemetry GenAI JSON:
   - Main-agent and LLM input messages (`gen_ai.input.messages`)
   - System instructions (`gen_ai.system_instructions`)
   - Tool definitions (`gen_ai.tool.definitions`)
   - Main-agent and LLM output messages (`gen_ai.output.messages`)
   - Final executed tool arguments (`gen_ai.tool.call.arguments`)
   - Successful tool results (`gen_ai.tool.call.result`)
   - Interaction spans retain the compatibility `new_context` attribute.

   Main-agent input is one original user-text projection before context
   expansion, and main-agent output is one final user-visible answer after all
   tool and continuation work settles. LLM values still come from provider-final
   SDK request objects and raw provider responses, so their input can include
   history, expanded files, system instructions, and tool results, and their
   output can include every provider candidate. Tool values come from the final
   invocation parameters and successful model-facing result. Each
   standard GenAI value is compact JSON and must be complete and schema-valid.
   A value that is invalid, cyclic, or longer than
   `sensitiveSpanAttributeMaxLength` is omitted as a whole; JSON is never
   truncated and no preview, hash, or truncation metadata is emitted. The
   interaction-specific `new_context` attribute retains its existing
   truncation behavior. The default maximum is 1 MiB (`1048576`) per attribute
   and the accepted range is `1..104857600` (100 MiB). The limit is measured as
   JavaScript string length rather than UTF-8 bytes. Non-ASCII content can
   therefore occupy more bytes after OTLP export.

2. **Log-to-span bridge spans** (used when HTTP traces are exported without a
   logs endpoint) retain `function_args`, `error`, `error.message`, and
   `error_message`, plus `prompt`, `request_text`, and `response_text` when
   `logPrompts` is also enabled, instead of dropping these attributes.

⚠️ **Security warning:** enabling this flag streams full conversation history,
file contents read by `read_file`, shell commands and their output (including
secrets in env vars or arguments), and model responses to the configured OTLP
backend. Treat the backend as a privileged data sink. The flag defaults to
`false`.

**Cost / payload size:** At the default limit, one LLM span can carry at most about 4 MiB across input, output, system instructions, and tool definitions; one Tool span can carry about 2 MiB across arguments and result; and one interaction can carry about 3 MiB across Agent input, Agent output, and compatibility `new_context`. This is Qwen Code's application-side cap, not a guarantee that every collector or backend accepts a single attribute that large. If spans are rejected or dropped, lower `sensitiveSpanAttributeMaxLength` (for example, to `61440`) and monitor exporter throughput.

This setting does not disable sensitive data in OTel logs or other telemetry
sinks; non-internal API response telemetry can populate `response_text`, so
OTel logs, UI telemetry, and chat recording may receive response text
independently of this setting. QwenLogger does not include `response_text`.

**HTTP OTLP encoding:** `otlpProtocol: "http"` sends OTLP/JSON with
`Content-Type: application/json` for traces, logs, and metrics. Use a collector
or receiver that supports OTLP/HTTP JSON. The `http` value does not select
OTLP/HTTP protobuf encoding.

**HTTP OTLP signal routing:** When using HTTP protocol (`otlpProtocol: "http"`),
Qwen Code automatically appends signal-specific paths (`/v1/traces`, `/v1/logs`,
`/v1/metrics`) to the base `otlpEndpoint`. For example, `http://collector:4318`
becomes `http://collector:4318/v1/traces` for traces. If the URL already ends
with a signal path, it is used as-is. Per-signal endpoint overrides
(`otlpTracesEndpoint`, etc.) take precedence over the base endpoint and are used
verbatim. gRPC protocol uses service-based routing and does not append paths.

The per-signal endpoint environment variables also accept the standard
OpenTelemetry names: `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,
`OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`, `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`.
The `QWEN_TELEMETRY_OTLP_*` variants take precedence over the `OTEL_*` variants.

**End-user identity:** `telemetry.userId` and
`QWEN_TELEMETRY_USER_ID` are explicit opt-ins for the ARMS span attribute
`gen_ai.user.id`. The environment variable takes precedence after both values
are trimmed; a blank environment value falls back to settings. The identifier
is written only to interaction, LLM, Tool, and Agent spans. It is not a Resource
attribute, log or metric attribute, outbound Baggage value, or current
OpenTelemetry GenAI standard field. Prefer a stable pseudonymous identifier.
The value is resolved at startup, so configuration changes require a restart.
Do not configure a process-wide value on a daemon or channel instance serving
multiple end users.

For detailed information about all configuration options, see the
[Configuration Guide](../../users/configuration/settings.md).

### Resource attributes

Resource attributes are static key-value pairs attached to every span, log,
and metric exported via OTLP. Use them to slice telemetry by team, environment,
deployment region, or any other dimension your backend cares about.

Two sources, merged in priority order (lowest → highest):

1. The standard `OTEL_RESOURCE_ATTRIBUTES` env var
2. `telemetry.resourceAttributes` in `.qwen/settings.json` (overrides env on
   key conflict)

`OTEL_SERVICE_NAME` is a separate escape hatch — when set, it overrides
`service.name` from any other source (per the OpenTelemetry spec).

#### Examples

**Slice all telemetry by team / environment:**

```bash
export OTEL_RESOURCE_ATTRIBUTES="team=platform,env=prod,cost_center=eng-123"
```

**Route to a per-tenant collector via `service.name`:**

```bash
export OTEL_SERVICE_NAME=qwen-code-ci
```

**Fleet baseline (`~/.qwen/settings.json`) + per-host override:**

```json
{
  "telemetry": {
    "resourceAttributes": {
      "deployment.environment": "production",
      "service.namespace": "engineering-tooling"
    }
  }
}
```

```bash
# Add a one-off tag without touching settings:
export OTEL_RESOURCE_ATTRIBUTES="debug_run=true"
```

#### Reserved keys

Some keys are runtime-controlled and cannot be overridden:

- `service.version` — always set to the running CLI version. Setting it from
  any source is silently dropped with a warning.
- `session.id` — runtime-injected per session. User-provided values from
  either env or settings are dropped with a warning. The reason is that
  Resource attributes auto-attach to every metric data point; allowing user
  override would bypass [Cardinality controls](#cardinality-controls) below.
  Spans and logs always carry `session.id`.

`service.name` is **not** reserved; it follows the precedence chain above.

#### Format

`OTEL_RESOURCE_ATTRIBUTES` follows the OpenTelemetry spec:
`key1=value1,key2=value2` with values percent-encoded. Spaces in values must
be encoded as `%20`, **commas as `%2C`** (unencoded commas split the value at
the wrong boundary and the second half is dropped as malformed). Malformed
pairs are skipped with a warning rather than failing telemetry startup.

#### Troubleshooting: when a user-provided attribute appears not to take effect

Reserved keys (`service.version`, `session.id`), malformed pairs, non-string
settings values, and invalid percent-encoding are all silently dropped with a
warning logged via the OpenTelemetry diagnostics channel. That channel routes
to the debug log file (`~/.qwen/log/otel-*.log`), **not** the console, so the
behavior can look like silent failure.

If a custom resource attribute isn't appearing on exported telemetry:

1. Check `~/.qwen/log/otel-*.log` for lines matching `cannot override` (reserved
   key dropped), `Skipping malformed` (bad env var pair), or `must be a string`
   (non-string settings value).
2. Verify the env var is set in the qwen-code process's environment (not just
   your shell) and that values are percent-encoded.
3. Confirm `telemetry.enabled` is `true` — telemetry init only runs if enabled.

### Cardinality controls

Metrics are aggregated by attribute set at the backend — every distinct
combination of attribute values produces a new time series. Attaching a
high-cardinality field like `session.id` to a metric causes time-series fan-out
proportional to the number of sessions, which quickly exhausts metric backend
storage.

To prevent this, Qwen Code keeps high-cardinality attributes off metric data
points by default. Spans and logs are per-event and unaffected, so they
continue to carry `session.id` for trace and log correlation.

#### `telemetry.metrics.includeSessionId` (default: `false`)

Setting this to `true` (via settings or
`QWEN_TELEMETRY_METRICS_INCLUDE_SESSION_ID=true`) re-attaches `session.id` to
every metric data point.

⚠️ **Warning:** each CLI session creates a new value. Leaving this on for a
fleet will blow up metric storage. Recommended only for short-term debugging.
For long-term session correlation, query trace or log backends instead.

#### Migration from earlier versions

Prior to this release, `session.id` was attached to metrics by default. If
your Prometheus queries / Grafana dashboards / alert rules reference
`session_id` on a metric, you have two options:

**Option A** — restore the previous behavior for short-term debugging:

```bash
export QWEN_TELEMETRY_METRICS_INCLUDE_SESSION_ID=true
```

or:

```json
{
  "telemetry": {
    "metrics": { "includeSessionId": true }
  }
}
```

**Option B (recommended)** — move session-level analysis off metrics. Spans
and logs still carry `session.id`, and trace / log backends (Jaeger, Tempo,
Loki, Aliyun SLS / ARMS Tracing) handle per-session slicing natively without
cardinality pressure.

### Client-side HTTP span on outbound fetch

When telemetry is enabled, Qwen Code registers `UndiciInstrumentation`
which creates a client-side HTTP span for every outbound `fetch()`
request originated by the process — including the LLM SDKs (`openai`,
`@google/genai`, `@anthropic-ai/sdk`), the MCP StreamableHTTP client, the
`WebFetch` tool, and any IDE-extension out-of-process calls. The span
lets you see network latency (TTFB / response body transfer) separately
from upstream model processing time, which the existing
`api.generateContent` span alone can't distinguish.

These spans go to your **own** OTLP collector (or file outfile) just like
the rest of the telemetry — they do not affect what is written onto the
outbound HTTP request itself. Whether the W3C `traceparent` header is
also written into the outgoing request stream is controlled by a
**separate, security-relevant setting** documented in
[outbound correlation](#outbound-correlation-security-relevant) below.

**Feedback-loop avoidance.** OTel SDK uses `fetch` internally to upload OTLP
data. Without protection, instrumenting `fetch` would trace those uploads,
which would themselves be uploaded, causing an infinite loop. Qwen Code's
undici instrumentation is configured with an `ignoreRequestHook` that skips
URLs matching the configured `telemetry.otlpEndpoint` /
`telemetry.otlpTracesEndpoint` / `telemetry.otlpLogsEndpoint` /
`telemetry.otlpMetricsEndpoint` prefixes. In file-outfile mode there are no
outbound HTTP uploads, so the hook is a no-op.

## Outbound correlation (SECURITY-RELEVANT)

These settings live in a **separate top-level namespace** from `telemetry.*`
on purpose: telemetry controls data flow into the operator's own
observability backend, while `outboundCorrelation.*` controls what
client-side correlation data qwen-code writes **into outbound LLM API
request streams** that reach third-party LLM provider endpoints
(DashScope, OpenAI, Anthropic, etc.). Different recipients, different
consent decision. **All values default to off.** See PR #4390 review
discussion for the framing rationale.

### `outboundCorrelation.propagateTraceContext`

```jsonc
"outboundCorrelation": {
  "propagateTraceContext": false // default
}
```

When `false` (default), Qwen Code installs a no-op `TextMapPropagator` on
the OTel SDK. UndiciInstrumentation still creates client HTTP spans for
your OTLP collector, but `propagation.inject()` is a no-op so **no
`traceparent` is written onto outbound requests**. Trace IDs stay
internal to the operator's collector.

When `true`, the SDK's default W3C composite propagator
(`tracecontext` + `baggage`) is installed and the standard `traceparent`
header is written on every outbound `fetch`:

```
traceparent: 00-<32-hex traceId>-<16-hex parentSpanId>-<01-sampled | 00-not-sampled>
```

Additionally, `TRACEPARENT` and `TRACESTATE` environment variables are set in
shell child processes (Bash tool, hooks, monitor) so that spawned commands can
participate in the same distributed trace.

Opt in only when the LLM provider also reports into your OTel collector
for cross-process trace stitching — e.g. ARMS Tracing serving DashScope.
For most operators the value is `false`; cross-vendor trace continuation
is niche.

**Depends on `telemetry.enabled: true`.** The OTel SDK only initializes
when telemetry is enabled, so `propagateTraceContext` only takes effect
in that state. Setting it to `true` while telemetry is disabled is a
silent no-op — no SDK, no propagator, no `traceparent` on the wire.
Verify both flags when wiring an ARMS+DashScope correlation setup:

```jsonc
{
  "telemetry": {
    "enabled": true,
    "otlpTracesEndpoint": "http://tracing-analysis-...",
  },
  "outboundCorrelation": {
    "propagateTraceContext": true,
  },
}
```

### Routify session affinity

Qwen Code's LLM requests through the OpenAI-compatible, DashScope, Anthropic,
Gemini, and Vertex provider paths include the current Qwen Code session ID in
the `session_id` header when addressed over HTTPS directly to `routify.alibaba-inc.com`,
`routify-online.alibaba-inc.com`, or `routify-pub.alibaba-inc.com`. Routify's
ModelRouter uses this value for session affinity and traffic marking. This
behavior is not controlled by `telemetry.enabled` or
`outboundCorrelation.*`.

The initial destination match is deliberately narrow: Qwen Code does not
attach the header to subdomains, other `alibaba-inc.com` hosts, or other LLM
endpoints. The standard fetch redirect behavior still applies after that
match, so a Routify response can forward the header by redirecting the request.

The session ID is read for every request, so a new session created by
`/clear` gets a new affinity value without rebuilding the SDK client. Gemini
requires an explicit Routify `baseUrl` so Qwen Code can verify the
destination.

`X-Qwen-Code-Request-Id` is not implemented.

## Inbound correlation (daemon HTTP API)

The daemon HTTP API accepts the standard W3C `traceparent` header on every
request. Two consumers read it independently:

- **Request span re-parenting (telemetry enabled).** When the telemetry SDK
  is initialized, a valid header is extracted as the request span's remote
  parent, so daemon spans attach under the caller's trace instead of
  starting a new one. The `_meta` forwarding path reads the same parent
  chain, so session subprocess spans forwarded through a daemon request
  inherit it too.
- **Access-log `traceId` field (both modes).** A dedicated pre-auth capture
  middleware parses the header on every request — including ones
  short-circuited at auth (401), the rate limiter (429), the JSON body
  parser (400), or never matched by any route (404) — and the access log
  emits the caller trace id as a camelCase `traceId` field. With telemetry
  disabled this field is the only join between a daemon log line and the
  caller's logs (or trace backend), so one saved query works for both modes
  with no telemetry configuration.

An invalid-but-present header is rejected (the span stays parentless) and
leaves a rate-limited DEBUG breadcrumb
(`qwen-code.daemon.traceparent.invalid`) recording the rejected value, so a
broken cross-service join is diagnosable from daemon logs alone.

### Forced sampling under inbound parents

Under the default `parentbased_always_on` sampler (and other parentbased
defaults), a remote parent's `sampled=0` flag is a head-based decision on
the caller's side, not a request to drop daemon telemetry, so extraction
forces the SAMPLED flag on inbound parents. The only opt-out is
`OTEL_TRACES_SAMPLER=parentbased_always_off`, which honors the caller's
flags — note it also disables root-span sampling for the whole daemon, not
just inbound-linked requests.

**Warning:** a constant `traceparent` (e.g. hardcoded in a load-test
client) re-parents every daemon request into one single trace; generate a
fresh header per request.

## Aliyun Telemetry

### Manual OTLP Export

To view Qwen Code telemetry in Alibaba Cloud Managed Service for
OpenTelemetry, configure Qwen Code to export to the OTLP endpoint
provided by ARMS.

Setting `"target": "gcp"` alone does not configure the export
destination. If `otlpEndpoint` is not set, Qwen Code still defaults to
`http://localhost:4317`. If `outfile` is set, it overrides
`otlpEndpoint` and telemetry is written to the file instead of being
sent to Alibaba Cloud.

1. Enable telemetry in your `.qwen/settings.json` and set the OTLP
   endpoint:

   **Option A: gRPC protocol** (standard OTLP endpoint):

   ```json
   {
     "telemetry": {
       "enabled": true,
       "target": "gcp",
       "otlpEndpoint": "https://<your-otlp-endpoint>",
       "otlpProtocol": "grpc"
     }
   }
   ```

   **Option B: HTTP protocol with per-signal endpoints** (for backends
   that use non-standard paths, e.g., `/api/otlp/traces` instead of
   `/v1/traces`):

   ```json
   {
     "telemetry": {
       "enabled": true,
       "otlpProtocol": "http",
       "otlpTracesEndpoint": "http://<host>/<token>/api/otlp/traces",
       "otlpLogsEndpoint": "http://<host>/<token>/api/otlp/logs",
       "otlpMetricsEndpoint": "http://<host>/<token>/api/otlp/metrics"
     }
   }
   ```

   > **Note:** When using HTTP protocol with only `otlpEndpoint` (no
   > per-signal overrides), Qwen Code appends standard OTLP paths
   > (`/v1/traces`, `/v1/logs`, `/v1/metrics`) to the base URL. If your
   > backend uses different paths, use per-signal endpoint overrides as
   > shown in Option B.

   To populate ARMS Session Analysis `User ID`, add a stable pseudonymous
   identity as a span-level setting:

   ```json
   {
     "telemetry": {
       "userId": "user-079458",
       "resourceAttributes": {
         "acs.arms.service.feature": "genai_app"
       }
     }
   }
   ```

   For container deployments, set
   `QWEN_TELEMETRY_USER_ID=user-079458` instead. A custom
   `telemetry.resourceAttributes.user.id` remains an unrelated Resource
   dimension and does not populate ARMS Session Analysis; remove it when
   migrating to the span-level setting.

2. If your Alibaba Cloud endpoint requires authentication, provide OTLP
   headers through standard OpenTelemetry environment variables such as
   `OTEL_EXPORTER_OTLP_HEADERS` (or the signal-specific variants). Qwen
   Code does not currently expose OTLP auth headers directly in
   `.qwen/settings.json`.
3. Run Qwen Code and send prompts.
4. View telemetry in Managed Service for OpenTelemetry:
   - Product overview:
     [What is Managed Service for OpenTelemetry?][aliyun-opentelemetry-overview]
   - Getting started:
     [Get started with Managed Service for OpenTelemetry][aliyun-opentelemetry-get-started]
   - Console entry points:
     - China mainland:
       [trace.console.aliyun.com][aliyun-opentelemetry-console-cn]
       (legacy console:
       [tracing.console.aliyun.com][aliyun-opentelemetry-console-cn-legacy])
     - International:
       [arms.console.alibabacloud.com][aliyun-opentelemetry-console-intl]
   - In the console, use `Applications` to inspect traces and service
     topology.
   - To locate the OTLP endpoint and access information:
     - **New console** (`trace.console.aliyun.com` or international):
       navigate to `Integration Center`.
     - **Legacy console** (`tracing.console.aliyun.com`): navigate to
       `Cluster Configurations` → `Access point information`.

## Local Telemetry

For local development and debugging, you can capture telemetry data locally:

### File-based Output (Recommended)

1. Enable telemetry in your `.qwen/settings.json`:

   ```json
   {
     "telemetry": {
       "enabled": true,
       "outfile": ".qwen/telemetry.log"
     }
   }
   ```

   > **Note:** When `outfile` is set, OTLP export is automatically disabled.
   > The `target` and `otlpEndpoint` settings are not needed for file-only
   > output and can be safely omitted from your config.

2. Run Qwen Code and send prompts.
3. View logs and metrics in the specified file (e.g., `.qwen/telemetry.log`).

### Collector-Based Export (Advanced)

1. Run the automation script:
   ```bash
   npm run telemetry -- --target=local
   ```
   This will:
   - Download and start Jaeger and OTEL collector
   - Configure your workspace for local telemetry
   - Provide a Jaeger UI at http://localhost:16686
   - Save logs/metrics to `~/.qwen/tmp/<projectHash>/otel/collector.log`
   - Stop collector on exit (e.g. `Ctrl+C`)
2. Run Qwen Code and send prompts.
3. View traces at http://localhost:16686 and logs/metrics in the collector log
   file.

## Logs and Metrics

The following section describes the structure of logs, metrics, and spans
generated for Qwen Code.

- A `sessionId` is included as a common attribute on all logs and metrics.

### Logs

Logs are timestamped records of specific events. All log records automatically include `event.name` and `event.timestamp` attributes.

The following events are logged:

#### Core Session Events

- `qwen-code.config`: Emitted once at startup with CLI configuration.
  - **Attributes**: `model`, `sandbox_enabled`, `core_tools_enabled`, `approval_mode`, `file_filtering_respect_git_ignore`, `debug_mode`, `truncate_tool_output_threshold`, `truncate_tool_output_lines`, `hooks` (comma-separated, omitted if disabled), `ide_enabled`, `interactive_shell_enabled`, `mcp_servers`, `mcp_servers_count`, `mcp_tools`, `mcp_tools_count`, `output_format`, `skills`, `subagents`

- `session.start`: A session begins. Emitted after telemetry initialization at startup and again on every session switch; lifecycle semantics are described in the Spans section.
  - **Attributes**: `session.id` (string), `session.previous_id` (string, present only when this start continues a persisted conversation under a new session id)

- `session.end`: A session ends. Emitted before a session switch replaces the current session, and at telemetry shutdown.
  - **Attributes**: `session.id` (string)

- `qwen-code.user_prompt`: User submits a prompt.
  - **Attributes**: `prompt_length` (int), `prompt_id` (string), `prompt` (string, excluded if `log_prompts_enabled` is false), `auth_type` (string)

- `qwen-code.user_retry`: User retries the last prompt.
  - **Attributes**: `prompt_id` (string)

- `qwen-code.conversation_finished`: A conversation turn sequence completes.
  - **Attributes**: `approvalMode` (string), `turnCount` (int)

- `qwen-code.user_feedback`: User submits session feedback.
  - **Attributes**: `session_id` (string), `rating` (int: 1=bad, 2=fine, 3=good), `model` (string), `approval_mode` (string), `prompt_id` (string, optional)

#### Tool Events

- `qwen-code.tool_call`: Each function/tool call. Terminal events are normalized so `status` is authoritative: success and cancelled events omit error fields, while error events always have a non-empty `error_type` (`unknown` when the producer did not classify the error). Blank tool names are emitted as `unknown_tool`. A missing `execution_status` is normalized to `unknown` and is never inferred from the terminal `status`.
  - **Attributes**: `function_name` (string), `function_args` (object), `call_id` (string, optional), `duration_ms` (int), `started_at_ms` (int, optional — epoch ms at which `duration_ms` started counting, including any approval wait; absent when the call never ran), `status` (string: "success", "error", or "cancelled"), `execution_status` (string: "not_started", "success", "error", "cancelled", or "unknown"), `success` (boolean), `decision` (string: "accept", "reject", "auto_accept", or "modify", optional), `error` (string, optional), `error_type` (string, present for error events), `prompt_id` (string), `response_id` (string, optional), `content_length` (int, optional), `tool_type` (string: "native" or "mcp"), `mcp_server_name` (string, optional), `metadata` (object, optional — for file-writing tools contains `model_added_lines`, `model_removed_lines`, `user_added_lines`, `user_removed_lines`, `model_added_chars`, `model_removed_chars`, `user_added_chars`, `user_removed_chars`)

- `qwen-code.file_operation`: Each file operation.
  - **Attributes**: `tool_name` (string), `operation` (string: "create", "read", "update"), `lines` (int, optional), `mimetype` (string, optional), `extension` (string, optional), `programming_language` (string, optional)

- `qwen-code.tool_output_truncated`: Tool output exceeded size threshold.
  - **Attributes**: `tool_name` (string), `original_content_length` (int), `truncated_content_length` (int), `threshold` (int), `lines` (int), `prompt_id` (string)

#### API Events

- `qwen-code.api_request`: Outgoing request to the LLM API.
  - **Attributes**: `model` (string), `prompt_id` (string), `request_text` (string, optional — contains request content only when `log_prompts_enabled` is true; log-to-span bridge spans additionally require `includeSensitiveSpanAttributes`; opaque `thoughtSignature` provider payload is included, with its policy decision tracked in #11682), `subagent_name` (string, optional)

- `qwen-code.api_response`: Response received from LLM API.
  - **Attributes**: `response_id` (string), `model` (string), `status_code` (int/string, optional), `duration_ms` (int), `input_token_count` (int), `output_token_count` (int), `cached_content_token_count` (int), `thoughts_token_count` (int), `total_token_count` (int), `prompt_id` (string), `auth_type` (string, optional), `response_text` (string, optional — contains visible response content only when `log_prompts_enabled` is true; log-to-span bridge spans additionally require `includeSensitiveSpanAttributes`; it carries no content for internal prompt ids or responses with no visible text), `subagent_name` (string, optional)

- `qwen-code.api_error`: API request failed.
  - **Attributes**: `model` (string), `prompt_id` (string), `duration_ms` (int), `error_message` (string), `response_id` (string, optional), `auth_type` (string, optional), `error_type` (string, optional), `status_code` (int/string, optional), `subagent_name` (string, optional)

  Additionally, OTel-standard aliases (`http.status_code`, `error.message`, `model_name`, `duration`) are emitted for compatibility.

- `qwen-code.api_cancel`: API request cancelled by user.
  - **Attributes**: `model` (string), `prompt_id` (string), `auth_type` (string, optional), `loop_wakeups_cancelled` (int, optional)

- `qwen-code.api_retry`: HTTP-status retry (429/5xx) at an LLM call site. Distinct from `chat.content_retry` which handles `InvalidStreamError` retries on a separate budget.
  - **Attributes**: `model` (string), `prompt_id` (string, optional), `attempt_number` (int), `error_type` (string, optional), `error_message` (string), `status_code` (int/string, optional), `retry_delay_ms` (int), `duration_ms` (int, equals retry_delay_ms — backoff sleep, not HTTP round-trip; for attempt duration see the qwen-code.llm_request span), `subagent_name` (string, optional)

- `qwen-code.malformed_json_response`: `generateJson` response couldn't be parsed.
  - **Attributes**: `model` (string)

- `qwen-code.flash_fallback`: Switched to flash model as fallback.
  - **Attributes**: `auth_type` (string)

- `qwen-code.ripgrep_fallback`: Switched to grep as fallback.
  - **Attributes**: `use_ripgrep` (boolean), `use_builtin_ripgrep` (boolean), `error` (string, optional)

#### Resilience Events

- `qwen-code.chat.content_retry`: Content-error retry (e.g. empty stream).
  - **Attributes**: `attempt_number` (int), `error_type` (string), `retry_delay_ms` (int), `model` (string)

- `qwen-code.chat.content_retry_failure`: All content retries exhausted.
  - **Attributes**: `total_attempts` (int), `final_error_type` (string), `total_duration_ms` (int, optional), `model` (string)

- `qwen-code.chat.invalid_chunk`: Invalid chunk received from stream.
  - **Attributes**: `error.message` (string, optional)

#### Command & Extension Events

- `qwen-code.slash_command`: User executes a slash command.
  - **Attributes**: `command` (string), `subcommand` (string, optional), `status` (string: "success" or "error", optional)

- `qwen-code.slash_command.model`: User switches model via `/model` command.
  - **Attributes**: `model_name` (string)

- `qwen-code.skill_launch`: A skill is launched.
  - **Attributes**: `skill_name` (string), `success` (boolean), `prompt_id` (string)

- `qwen-code.extension_install`: Extension installed.
  - **Attributes**: `extension_name` (string), `extension_version` (string), `extension_source` (string), `status` (string: "success"/"error")

- `qwen-code.extension_uninstall`: Extension uninstalled.
  - **Attributes**: `extension_name` (string), `status` (string)

- `qwen-code.extension_enable`: Extension enabled.
  - **Attributes**: `extension_name` (string), `setting_scope` (string)

- `qwen-code.extension_disable`: Extension disabled.
  - **Attributes**: `extension_name` (string), `setting_scope` (string)

- `qwen-code.extension_update`: Extension updated.
  - **Attributes**: `extension_name` (string), `extension_id` (string), `extension_previous_version` (string), `extension_version` (string), `extension_source` (string), `status` (string: "success"/"error")

- `qwen-code.ide_connection`: IDE connection event.
  - **Attributes**: `connection_type` (string: "start" or "session")

- `qwen-code.auth`: Authentication event.
  - **Attributes**: `auth_type` (string), `action_type` ("auto", "manual", "coding-plan"), `status` ("success", "error", "cancelled"), `error_message` (optional)

#### Subagent Events

- `qwen-code.subagent_execution`: Subagent lifecycle event.
  - **Attributes**: `subagent_name` (string), `status` ("started", "completed", "failed", "cancelled"), `terminate_reason` (optional), `result` (optional), `execution_summary` (optional)

#### Goal Events

- `qwen-code.goal_state`: A reported Goal runtime transition, not one event per transcript record. Emitted for `create`, `replace`, `edit`, `pause`, `resume`, `clear`, `complete`, `blocked`, `usage_limited`, and `verifier_reject`. Per-turn `turn_finished` and `checkpoint` are not reported, and neither is the state a resumed session recovers from its transcript. A checkpoint check following a verifier rejection may be journaled as `verifier_reject` while broadcasting the excluded `checkpoint` cause; only the rejection transition is reported. The objective, the stop reason, and checkpoint failure text are never included in this event, whatever `telemetry.logPrompts` is set to. Tool-call events can still carry objectives and model-authored reasons in `function_args`.
  - **Scope**: `replace` describes the successor Goal and does not settle the outgoing Goal. `pause` includes both user and automatic no-progress pauses. `no_progress_turns` carries the raw consecutive no-progress count when available; it does not classify the stop or add a metric dimension. Outcome histograms include only `complete`, `blocked`, and `usage_limited`; they do not measure the final spend of every removed or paused Goal.
  - **Attributes**: `cause` (string), `goal_id` (string), `revision` (int), `status` ("active", "paused", "blocked", "usage_limited", "complete"; absent on `clear`), `limit_kind` ("evidence_catalog", "checkpoint_request", "token_budget", "turn_budget", "time_budget"; optional; the first two are no longer produced and can appear only on a Goal an earlier build stopped, since the verifier reads the transcript tail and no evidence checkpoint runs), `turn_count` (int, optional), `tokens_used` (int, optional; legacy transcripts without spend tracking restore this as 0, not as an absent value), `no_progress_turns` (int, optional), `token_budget` (int, optional), `turn_budget` (int, optional), `active_time_ms` (int, optional), `active_time_budget_ms` (int, optional), `objective_length` (int, optional; code points)

#### Arena Events

- `qwen-code.arena_session_started`: Arena session begins.
  - **Attributes**: `arena_session_id` (string), `model_ids` (JSON string array), `task_length` (int)

- `qwen-code.arena_agent_completed`: An arena agent finishes.
  - **Attributes**: `arena_session_id` (string), `agent_session_id` (string), `agent_model_id` (string), `status` (string: "completed"/"failed"/"cancelled"), `duration_ms` (int), `rounds` (int), `total_tokens` (int), `input_tokens` (int), `output_tokens` (int), `tool_calls` (int), `successful_tool_calls` (int), `failed_tool_calls` (int)

- `qwen-code.arena_session_ended`: Arena session completes.
  - **Attributes**: `arena_session_id` (string), `status` (string: "selected"/"discarded"/"failed"/"cancelled"), `duration_ms` (int), `display_backend` (string, optional), `agent_count` (int), `completed_agents` (int), `failed_agents` (int), `cancelled_agents` (int), `winner_model_id` (string, optional)

#### Workflow Events

- `qwen-code.workflow_keyword`: Workflow keyword trigger fired.

- `qwen-code.workflow_run`: Workflow run reached terminal state.
  - **Attributes**: `status` (string), `agents_dispatched` (int), `agents_completed` (int, all settled dispatches), `agents_failed` (int, settled dispatches with failed status), `agents_cached` (int, settled dispatches served from a prior run), `agents_respawned` (int, dispatched calls re-run after a prior failed or interrupted attempt), `phase_count` (int), `tokens_spent` (int), `duration_ms` (int). `agents_failed` and `agents_cached` are subsets of `agents_completed`, while `agents_respawned` describes provenance and is not an additional outcome count.

- `qwen-code.workflow_size_warning`: A running workflow crossed its large-run threshold; emitted at most once per run.
  - **Attributes**: `axis` (string: "agents"/"tokens", the threshold crossed first), `scheduled_agents` (int, dispatches excluding journal replays), `total_tokens` (int), `projected_tokens` (int), `agent_cap` (int), `token_cap` (int), `cap_from_guideline` (boolean, whether the agent threshold came from `tools.workflowSizeGuideline`)

#### Auto-Memory Events

- `qwen-code.memory.extract`: Memory extraction run completed.
  - **Attributes**: `trigger` ("auto"/"manual"), `status` ("completed"/"skipped"/"failed"), `skipped_reason` (optional), `patches_count` (int), `touched_topics` (string), `duration_ms` (int)

- `qwen-code.memory.dream`: Memory consolidation (dream) run completed.
  - **Attributes**: `trigger` ("auto"/"manual"), `status` ("updated"/"noop"/"failed"/"cancelled"), `deduped_entries` (int), `touched_topics_count` (int), `touched_topics` (string), `duration_ms` (int)

- `qwen-code.memory.recall`: Memory recall operation completed.
  - **Attributes**: `query_length` (int), `docs_scanned` (int), `docs_selected` (int), `strategy` ("none"/"heuristic"/"model"), `duration_ms` (int), `selector_skipped` (bool, optional — set for structured-mode recalls that made a skip decision, so not on the empty-query/empty-corpus short circuit; false when the selector runs, including when the #13003 knob is off)

#### Prompt Suggestion & Speculation Events

- `qwen-code.prompt_suggestion`: Prompt suggestion outcome.
  - **Attributes**: `outcome` ("accepted"/"ignored"/"suppressed"), `prompt_id` (optional), `accept_method` ("tab"/"enter"/"right", optional), `accept_source` ("live"/"fallback", optional), `time_to_accept_ms` (optional), `time_to_ignore_ms` (optional), `time_to_first_keystroke_ms` (optional), `suggestion_length` (optional), `similarity` (optional), `was_focused_when_shown` (optional), `reason` (optional)

- `qwen-code.speculation`: Speculative execution outcome.
  - **Attributes**: `outcome` ("accepted"/"aborted"/"failed"), `turns_used` (int), `files_written` (int), `tool_use_count` (int), `duration_ms` (int), `boundary_type` (optional), `had_pipelined_suggestion` (boolean)

#### Other Events

- `qwen-code.chat_compression`: Chat context compressed.
  - **Attributes**: `tokens_before` (int), `tokens_after` (int), `compression_input_token_count` (int, optional), `compression_output_token_count` (int, optional)

- `qwen-code.next_speaker_check`: Next speaker determination.
  - **Attributes**: `prompt_id` (string), `finish_reason` (string), `result` (string)

- `loop_detected`: Loop detected during agent execution. _(Note: emitted without `qwen-code.` prefix — pre-existing inconsistency.)_
  - **Attributes**: `loop_type` (string), `prompt_id` (string), `loop_reason` (string, optional), `tool_call_count` (int, optional), `tool_call_limit` (int, optional)
  - `loop_reason` names the cause when one loop type covers several: `explicit_cap`, `backstop` or `stuck_repetition` for `turn_tool_call_cap`, and `repeated_call` or `provider_duplicate_id` for `global_tool_call_duplicate`. `tool_call_count` and `tool_call_limit` are set for `turn_tool_call_cap` only. `tool_call_count` is the count the cap compared with the limit; daemon/ACP sessions check the cap per batch before running it, so their count includes the whole skipped batch.

- `loop_protection`: A loop detector governed by `model.loopProtection` reached a reminder or stop point. _(Note: emitted without `qwen-code.` prefix, like `loop_detected`.)_
  - **Attributes**: `prompt_id` (string), `loop_type` (string), `decision` (string: `reminded`, `stopped` or `observed`), `mode` (string: `stop`, `warn` or `off`), `loop_reason` (string, optional)
  - `reminded` is logged when the reminder is sent to the model; `stopped` is logged once per turn and also as `loop_detected`; `observed` is logged once per pattern under `off`, and under `warn` when a stateful read tool reaches the stop point without the result evidence a reminder needs. Budget stops (an explicit cap, the backstop) and the duplicate provider tool-call id guard are not governed and log no `loop_protection` event.

- `kitty_sequence_overflow`: Kitty graphics protocol sequence exceeded buffer size. _(Note: emitted without `qwen-code.` prefix — pre-existing inconsistency.)_
  - **Attributes**: `sequence_length` (int), `truncated_sequence` (string, first 20 chars)

### Metrics

Metrics are numerical measurements of behavior over time. Metric names use the `qwen-code.*` prefix.

#### Core Metrics

- `qwen-code.session.count` (Counter, Int): Incremented once per CLI startup.

- `qwen-code.tool.call.count` (Counter, Int): Counts tool calls.
  - **Attributes**: `function_name`, `status` ("success"/"error"/"cancelled"), `success` (boolean, retained for compatibility), `decision` ("accept"/"reject"/"auto_accept"/"modify", optional), `tool_type` ("mcp"/"native", optional)

- `qwen-code.tool.execution.count` (Counter, Int): Counts tool execution outcomes. Deliberately carries no `function_name` dimension to stay low-cardinality, so an execution-failure rate cannot be attributed to a specific tool without dropping to the `qwen-code.tool_call` logs; exclude `unknown`, `not_started`, and `cancelled` when computing execution-failure ratios (denominator is `success` + `error`).
  - **Attributes**: `execution_status` ("not_started"/"success"/"error"/"cancelled"/"unknown"), `tool_type` ("mcp"/"native"), plus globally configured common metric attributes such as the opt-in `session.id`

- `qwen-code.tool.call.latency` (Histogram, ms): Measures tool call latency.
  - **Attributes**: `function_name` (string)

- `qwen-code.api.request.count` (Counter, Int): Counts all API requests.
  - **Attributes**: `model`, `status_code`, `error_type` (optional)

- `qwen-code.api.request.latency` (Histogram, ms): Measures API request latency.
  - **Attributes**: `model` (string)

- `qwen-code.token.usage` (Counter, Int): Counts tokens used.
  - **Attributes**: `model`, `type` ("input"/"output"/"thought"/"cache")

- `qwen-code.file.operation.count` (Counter, Int): Counts file operations.
  - **Attributes**: `operation` ("create"/"read"/"update"), `lines` (optional), `mimetype` (optional), `extension` (optional), `programming_language` (optional)

- `qwen-code.chat_compression` (Counter, Int): Counts chat compression operations.
  - **Attributes**: `tokens_before` (int), `tokens_after` (int)

- `qwen-code.slash_command.model.call_count` (Counter, Int): Counts model slash command calls.
  - **Attributes**: `slash_command.model.model_name` (string)

- `qwen-code.subagent.execution.count` (Counter, Int): Counts subagent execution events.
  - **Attributes**: `subagent_name`, `status` ("started"/"completed"/"failed"/"cancelled"), `terminate_reason` (optional)

#### Resilience Metrics

- `qwen-code.api.retry.count` (Counter, Int): HTTP-status retries (429/5xx) at LLM call sites.
  - **Attributes**: `model` (string)

- `qwen-code.chat.content_retry.count` (Counter, Int): Retries due to content errors.

- `qwen-code.chat.content_retry_failure.count` (Counter, Int): All content retries exhausted.

- `qwen-code.chat.invalid_chunk.count` (Counter, Int): Invalid chunks from stream.

#### Goal Metrics

- `qwen-code.goal.transition.count` (Counter, Int): Goal state transitions, one per `qwen-code.goal_state` event.
  - **Attributes**: `cause`, `status` (optional), `limit_kind` (optional)

- `qwen-code.goal.tokens_used` (Histogram, `{token}`): Cumulative tokens spent at each completion, blocking, or usage-limit stop. Legacy transcripts without spend tracking restore an initial spend of 0; unknown prior spend is not distinguishable from measured zero.
  - **Attributes**: `cause` ("complete", "blocked", "usage_limited"), `limit_kind` (optional)

- `qwen-code.goal.turn_count` (Histogram, `{turn}`): Cumulative turns finished at each of the same three stops.
  - **Attributes**: `cause` ("complete", "blocked", "usage_limited"), `limit_kind` (optional)

A resumed Goal can contribute multiple stop observations, each containing its lifetime cumulative meters. Histogram `_count` counts stops, not distinct Goals, and `_sum` is not total Goal spend. Token buckets extend through 600 million and turn buckets through 50,000 to distinguish larger resumed runs. Values above those finite boundaries share the `+Inf` bucket; observation count and sum still retain those values.

#### Arena Metrics

- `qwen-code.arena.session.count` (Counter, Int): Arena sessions by status.
  - **Attributes**: `status`, `display_backend` (optional)

- `qwen-code.arena.session.duration` (Histogram, ms): Arena session duration.
  - **Attributes**: `status`

- `qwen-code.arena.agent.count` (Counter, Int): Arena agent completions.
  - **Attributes**: `status`, `model_id`

- `qwen-code.arena.agent.duration` (Histogram, ms): Arena agent execution duration.
  - **Attributes**: `model_id`

- `qwen-code.arena.agent.tokens` (Counter, Int): Token usage by arena agents.
  - **Attributes**: `model_id`, `type` ("input"/"output")

- `qwen-code.arena.result.selected` (Counter, Int): Arena result selections.
  - **Attributes**: `model_id`

#### Auto-Memory Metrics

- `qwen-code.memory.extract.count` (Counter, Int): Auto-memory extraction runs.
  - **Attributes**: `trigger` ("auto"/"manual"), `status`

- `qwen-code.memory.extract.duration` (Histogram, ms): Extraction duration.
  - **Attributes**: `trigger`, `status`

- `qwen-code.memory.dream.count` (Counter, Int): Auto-memory dream runs.
  - **Attributes**: `trigger` ("auto"/"manual"), `status`

- `qwen-code.memory.dream.duration` (Histogram, ms): Dream run duration.
  - **Attributes**: `trigger`, `status`

- `qwen-code.memory.recall.count` (Counter, Int): Auto-memory recall operations.
  - **Attributes**: `strategy` ("none"/"heuristic"/"model"), `selector_skipped` (bool, optional — attached only for structured-mode recalls while the #13003 skip-selector knob is on)

- `qwen-code.memory.recall.duration` (Histogram, ms): Recall duration.
  - **Attributes**: `strategy`, `selector_skipped` (bool, optional — same conditions as the counter)

#### API Request Breakdown

- `qwen-code.api.request.breakdown` (Histogram, ms): API request time breakdown by phase.
  - **Attributes**: `model`, `phase` ("request_preparation"/"network_latency"/"response_processing"/"token_processing")

### Daemon Metrics

The daemon process (long-running HTTP server mode) exposes its own metrics.

> **Note:** The three Observable Gauges (`daemon.session.active`, `daemon.sse.active`, `daemon.process.heap_used`) are callback-based metrics updated at each collection interval; `registerDaemonGaugeCallbacks()` must be invoked during daemon initialization to register the observation callbacks.

#### HTTP

- `qwen-code.daemon.http.request.count` (Counter, Int): Request count by route and status class.
  - **Attributes**: `route`, `status_class` ("2xx"/"4xx"/"5xx")

- `qwen-code.daemon.http.request.duration` (Histogram, ms): Request duration.
  - **Attributes**: `route`
  - **Buckets**: 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000

#### Sessions

- `qwen-code.daemon.session.active` (ObservableGauge, Int): Current active sessions.

- `qwen-code.daemon.session.lifecycle` (Counter, Int): Session lifecycle events.
  - **Attributes**: `action` ("spawn"/"close"/"die")

#### Channels

- `qwen-code.daemon.channel.lifecycle` (Counter, Int): ACP channel lifecycle events.
  - **Attributes**: `action` ("spawn"/"exit"), `expected` (boolean, optional)

#### Prompts

- `qwen-code.daemon.prompt.queue_wait` (Histogram, ms): Prompt FIFO queue wait time.
  - **Buckets**: 1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 30000, 60000

- `qwen-code.daemon.prompt.duration` (Histogram, ms): End-to-end prompt duration.
  - **Buckets**: 100, 500, 1000, 2500, 5000, 10000, 30000, 60000, 120000, 300000, 600000

#### Errors

- `qwen-code.daemon.bridge.error.count` (Counter, Int): Bridge errors by type.
  - **Attributes**: `error_type` (known class name or "unknown")

- `qwen-code.daemon.cancel.count` (Counter, Int): Cancel request count.

#### Resources

- `qwen-code.daemon.sse.active` (ObservableGauge, Int): Active SSE connections.

- `qwen-code.daemon.process.heap_used` (ObservableGauge, Int, bytes): Heap memory usage.

### Spans

Distributed tracing spans form a tree rooted at `qwen-code.interaction`. In the CLI, each interaction is a trace root with its own `traceId`; ACP and daemon paths may inherit an inbound parent context. Cross-prompt correlation uses the `session.id` attribute.

Session lifecycle is also exported through the OpenTelemetry General Session
semantic conventions. When the OTel logs pipeline is enabled, Qwen Code emits
`session.start` and `session.end` log events with the required `session.id`
attribute (cataloged under Core Session Events above). A resumed persisted
conversation includes `session.previous_id` on its `session.start` event only
when the resumed session id differs from the current one; cold-start
resumptions (`--resume`, `--continue`, `--fork-session`) do not carry it.
`/clear` and other replacement flows intentionally do not claim continuation
because they discard the previous conversation.

The existing Qwen-specific `qwen-code.config`/`cli_config` and RUM
`session_start` records remain available for compatibility. GenAI request
spans continue to use `gen_ai.conversation.id` for the same owning session ID.

- `qwen-code.interaction`: Main-agent invocation span. It covers all LLM requests, tool approval/execution, and continuations for one logical prompt. User queries, retries, cron prompts, notifications, teammate messages, and Goal turns create invocations; tool results, hooks, and steering reuse the exact active prompt ID.
  - **GenAI attributes**: `gen_ai.operation.name` (`invoke_agent`), `gen_ai.agent.name` (`qwen-code`), `gen_ai.conversation.id`, optional `gen_ai.output.type` (`json` only with a configured JSON Schema), sensitive `gen_ai.input.messages`, sensitive `gen_ai.output.messages`, and optional ARMS extension `gen_ai.user.id`
  - **Compatibility attributes**: `session.id`, `qwen-code.prompt_id`, `qwen-code.message_type`, `qwen-code.model`, `qwen-code.approval_mode`, `interaction.sequence`, `interaction.duration_ms`, `qwen-code.turn_status` ("ok"/"error"/"cancelled")
  - `gen_ai.request.model` is intentionally omitted because the agent supports overrides, fallback, and dynamic model selection. `gen_ai.provider.name` and agent ID/version/description are also omitted.
  - Agent input is one original user prompt, not the expanded model request. Agent output is one final user-visible text projection; structured JSON uses compact JSON text with `finish_reason=tool_call`. Both are omitted unless sensitive span attributes are enabled and the complete JSON fits the per-attribute limit.

- `qwen-code.llm_request`: Wraps a single LLM API call.
  - **GenAI attributes**: `gen_ai.operation.name`, `gen_ai.provider.name`, `gen_ai.conversation.id`, optional ARMS extension `gen_ai.user.id`, `gen_ai.request.model`, `gen_ai.request.stream`, `gen_ai.request.choice.count`, `gen_ai.request.max_tokens`, `gen_ai.request.temperature`, `gen_ai.request.top_p`, `gen_ai.request.frequency_penalty`, `gen_ai.request.presence_penalty`, `gen_ai.request.stop_sequences`, `gen_ai.request.reasoning.level`, optional `gen_ai.output.type`, `gen_ai.response.id`, `gen_ai.response.model`, `gen_ai.response.finish_reasons`, `gen_ai.response.time_to_first_chunk`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `gen_ai.usage.cache_read.input_tokens`, `gen_ai.usage.cache_creation.input_tokens`
  - **Compatibility attributes**: `session.id`, `qwen-code.prompt_id`, `llm_request.context` ("subagent"/"interaction"/"standalone"), `duration_ms`, `ttft_ms`, `request_setup_ms`, `attempt`, `retry_total_delay_ms`, `sampling_ms`, `output_tokens_per_second`, `success`, `error`, `finish_reason`, `thoughts_token_count`, `subagent_name`, `error_type`, `error_status_code`
  - Standard response fields come from the provider response. Standard token fields are emitted only for provider-reported non-negative safe integers. If the provider reports only a total token count, input/output usage is omitted rather than estimated.
  - Standard request-parameter fields come from the first provider-final SDK request object after adapter defaults, overrides, unsupported-field removal, and output-window clamps. Qwen Code does not infer SDK or server defaults.
  - `gen_ai.request.reasoning.level` preserves the exact non-empty `reasoning_effort` string from the first final OpenAI-compatible request, including provider-specific tiers and `none` when sent. It is omitted when absent or invalid, including when the adapter removes it; nested provider-specific reasoning fields are not mapped. This request parameter does not require sensitive-content capture.
  - Streaming requests emit `gen_ai.request.stream=true`. `gen_ai.response.time_to_first_chunk` measures seconds from the provider call to the first normalized response yielded by the provider adapter, which may differ from the first raw network frame. Non-streaming requests omit both standard streaming attributes because an absent `gen_ai.request.stream` means non-streaming in the semantic convention.

- `qwen-code.tool`: Wraps the full tool lifecycle (approval wait + execution).
  - **Attributes**: `session.id`, optional ARMS extension `gen_ai.user.id`, `gen_ai.operation.name` (`execute_tool`), optional inherited `gen_ai.agent.name`, `gen_ai.tool.name`, `gen_ai.tool.type` (`function`), `gen_ai.tool.call.id`, `tool.call_id`, `duration_ms`, `success`, `error`, `error.type` on failure, `tool.failure_kind` (string, optional — the specific failure reason, e.g. "cancelled", "tool_error", "tool_exception", "timeout", "permission_denied", "pre_hook_blocked")

- `qwen-code.tool.execution`: Wraps the tool execution phase (after approval). Emitted only for attempted executions.
  - **Attributes**: `session.id`, `gen_ai.tool.name` (optional), `tool.call_id` (optional), `duration_ms`, `success`, `error`, `execution_status` ("success"/"error"/"cancelled"), `error_type`, `error.type`

- `qwen-code.tool.blocked_on_user`: Time a tool spends waiting on user approval.
  - **Attributes**: `session.id`, `tool.name`, `tool.call_id`, `duration_ms`, `decision` ("proceed_once"/"proceed_always"/"cancel"/"aborted"/"auto_approved"/"error"), `source` ("cli"/"ide"/"hook"/"auto"/"system")

- `qwen-code.hook`: Wraps each pre/post-tool-use hook fire site.
  - **Attributes**: `session.id`, `hook_event` ("PreToolUse"/"PostToolUse"/"PostToolUseFailure"/"PostToolBatch"), `tool.name`, `tool.use_id` (optional), `is_interrupt` (boolean, optional), `duration_ms`, `success`, `should_proceed` (optional), `should_stop` (optional), `block_type` (optional), `error` (optional)

- `qwen-code.subagent`: Wraps a single subagent invocation.
  - **Attributes**: `gen_ai.operation.name` (`invoke_agent`), `gen_ai.agent.name`, `gen_ai.agent.description`, `gen_ai.conversation.id`, optional ARMS extension `gen_ai.user.id`, optional `gen_ai.request.model`, `qwen-code.subagent.id`, `qwen-code.subagent.name`, `qwen-code.subagent.invocation_kind` ("foreground"/"fork"/"background"), `qwen-code.subagent.is_built_in`, `qwen-code.subagent.depth`, `qwen-code.subagent.status`, `qwen-code.subagent.terminate_reason`, `qwen-code.subagent.duration_ms`

Successful and cancelled GenAI spans leave `SpanStatus` as `UNSET`. Failures set `ERROR`, a bounded status description, and low-cardinality `error.type`.

#### GenAI field migration and ARMS recognition

LLM spans now use standard `gen_ai.request.*`, `gen_ai.response.*`, and `gen_ai.usage.*` fields without exact-equivalent private aliases. Request sampling attributes are written only under their standard names; no bare `temperature`, `top_p`, `max_tokens`, penalty, choice-count, or stop-sequence aliases are emitted. Tool spans similarly use `gen_ai.tool.name` without `tool.name`; blocked-on-user and hook spans keep `tool.name` because they are not GenAI Tool spans. The invalid aliases `gen_ai.usage.cached_tokens`, `gen_ai.server.time_to_first_token`, and `gen_ai.usage.reasoning_tokens` are no longer emitted. Use `gen_ai.usage.cache_read.input_tokens` for provider-reported cache reads and `gen_ai.response.time_to_first_chunk` for standard streaming latency. The private `ttft_ms` Span attribute remains available for first-user-visible-output latency and continues driving `/stats`, `sampling_ms`, and output-token throughput; `gen_ai.response.time_to_first_chunk` is an independent standard attribute measuring first normalized chunk latency. The full version-pinned contract and deferred fields are documented in [GenAI and ARMS field alignment](../../design/gen-ai-arms-field-alignment.md).

To make ARMS recognize exported spans as a GenAI application, configure its resource feature explicitly:

```json
{
  "telemetry": {
    "resourceAttributes": {
      "acs.arms.service.feature": "genai_app"
    }
  }
}
```

Qwen Code does not inject this ARMS-specific resource attribute or `gen_ai.span.kind`. ARMS can infer LLM, Tool, and Agent roles from `gen_ai.operation.name`.

- `qwen-code.daemon.request`: Wraps a daemon HTTP request.
  - **Attributes**: `http.request.method`, `http.route`, `qwen-code.daemon.operation`, `session.id`, `http.response.status_code`

- `qwen-code.daemon.bridge`: Wraps daemon bridge operations.
  - **Attributes**: `qwen-code.daemon.operation`

#### Resource Metrics

- `qwen-code.memory.usage` (Histogram, bytes): Memory usage. Recorded by the memory-pressure monitor when telemetry is enabled.
  - **Attributes**: `memory_type` (string: "heap_used"/"rss")

- `qwen-code.cpu.usage` (Histogram, percent): CPU usage percentage. Recorded by the memory-pressure monitor when telemetry is enabled.
  - **Attributes**: (none)

### Performance Monitoring (Reserved)

The following metrics are defined but **not yet enabled in production**. They will be activated behind a dedicated performance monitoring config flag.

- `qwen-code.startup.duration` (Histogram, ms): CLI startup time by phase.
  - **Attributes**: `phase` (string)

- `qwen-code.tool.queue.depth` (Histogram, count): Tools in execution queue.

- `qwen-code.tool.execution.breakdown` (Histogram, ms): Tool execution time by phase.
  - **Attributes**: `function_name`, `phase` ("validation"/"preparation"/"execution"/"result_processing")

- `qwen-code.token.efficiency` (Histogram, ratio): Token efficiency metrics.
  - **Attributes**: `model`, `metric`, `context` (optional)

- `qwen-code.performance.score` (Histogram, score): Composite performance score (0-100).
  - **Attributes**: `category`, `baseline` (optional)

- `qwen-code.performance.regression` (Counter, Int): Regression detection events.
  - **Attributes**: `metric`, `severity` ("low"/"medium"/"high"), `current_value`, `baseline_value`

- `qwen-code.performance.regression.percentage_change` (Histogram, percent): Percentage change vs baseline.
  - **Attributes**: `metric`, `severity`, `current_value`, `baseline_value`

- `qwen-code.performance.baseline.comparison` (Histogram, percent): Performance vs baseline.
  - **Attributes**: `metric`, `category`, `current_value`, `baseline_value`

---

### Developers Examples Proxy Script (`docs/developers/examples/proxy-script.md`)

# Example Proxy Script

The following is an example of a proxy script that can be used with the `QWEN_SANDBOX_PROXY_COMMAND` environment variable. This script only allows `HTTPS` connections to `example.com:443` and declines all other requests.

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

### Developers Tools Exit Plan Mode (`docs/developers/tools/exit-plan-mode.md`)

# Exit Plan Mode Tool (`exit_plan_mode`)

This document describes the `exit_plan_mode` tool for Qwen Code.

## Description

Use `exit_plan_mode` when you are in plan mode and have finished presenting your implementation plan. This tool prompts the user to approve or reject the plan and transitions from planning mode to implementation mode.

The tool is specifically designed for tasks that require planning implementation steps before writing code. It should NOT be used for research or information-gathering tasks.

### Arguments

`exit_plan_mode` takes these arguments:

- `plan` (string, required): The implementation plan you want to present to the user for approval. This should be a concise, markdown-formatted plan describing the implementation steps.
- `originalRequest` (string, optional): The original request, used when a plan-required teammate asks its leader for approval.
- `researchSummary` (string, optional): Investigation context for a plan-required teammate's leader review.

## How to use `exit_plan_mode` with Qwen Code

The Exit Plan Mode tool is part of Qwen Code's planning workflow. When you're in plan mode (typically after exploring a codebase and designing an implementation approach), you use this tool to:

1. Present your implementation plan to the user
2. Request approval to proceed with implementation
3. Transition from plan mode to implementation mode based on user response

The tool will prompt the user with your plan and provide options to:

- **Restore Previous**: Approve the plan and restore the mode active before Plan mode
- **Proceed Once**: Approve the plan for this session only
- **Proceed Always**: Approve the plan and enable auto-approval for future edit operations
- **Cancel**: Reject the plan and remain in planning mode

Usage:

```
exit_plan_mode(plan="Your detailed implementation plan here...")
```

## When to Use This Tool

Use `exit_plan_mode` when:

1. **Implementation tasks**: You are planning the implementation steps for a coding task
2. **Plan completion**: You have finished exploring and designing your implementation approach
3. **User approval needed**: You need user confirmation before proceeding with code changes
4. **Code writing tasks**: The task involves writing, modifying, or refactoring code

### Examples of appropriate usage:

- "Help me implement user authentication" → Use after planning the auth system implementation
- "Add a new API endpoint for user management" → Use after designing the endpoint structure
- "Refactor the database layer to use TypeORM" → Use after planning the refactoring approach

## When NOT to Use This Tool

Do NOT use `exit_plan_mode` for:

1. **Research tasks**: Tasks focused on understanding or exploring existing code
2. **Information gathering**: When you're searching, reading, or analyzing without implementing
3. **Documentation tasks**: When creating documentation without code changes
4. **Analysis tasks**: When providing explanations or reviews without implementation

### Examples of inappropriate usage:

- "Search for and understand the implementation of vim mode in the codebase" → Don't use (research task)
- "Explain how the authentication system works" → Don't use (analysis task)
- "Find all TODO comments in the project" → Don't use (information gathering)

## `exit_plan_mode` examples

### Implementation planning example

```
exit_plan_mode(plan="## Implementation Plan for User Dashboard

### 1. Create Dashboard Components
- Create `UserDashboard.tsx` with main dashboard layout
- Add `DashboardStats.tsx` for displaying user statistics
- Implement `ActivityFeed.tsx` for recent user activities

### 2. Add API Integration
- Create `dashboardApi.ts` for dashboard data fetching
- Add endpoints for user stats and activity data
- Implement error handling and loading states

### 3. Update Routing
- Add dashboard route in `App.tsx`
- Update navigation component with dashboard link
- Add route protection for authenticated users

### 4. Testing and Documentation
- Write unit tests for dashboard components
- Add integration tests for API calls
- Update user documentation with dashboard features

This implementation will take approximately 4-6 hours and requires changes to 8-10 files.")
```

### Refactoring plan example

```
exit_plan_mode(plan="## Database Layer Refactoring Plan

### Phase 1: Setup and Configuration
- Install and configure TypeORM dependencies
- Create database connection configuration
- Set up entity definitions for existing models

### Phase 2: Migration
- Create migration scripts for existing data
- Implement new repository pattern using TypeORM
- Replace raw SQL queries with TypeORM query builder

### Phase 3: Testing and Validation
- Update all database tests to use TypeORM
- Validate data integrity after migration
- Performance testing to ensure no regressions

This refactoring will modernize our database layer while maintaining backward compatibility.")
```

## User Response Handling

After calling `exit_plan_mode`, the user can respond in several ways:

- **Proceed Once**: The plan is approved for immediate implementation with default confirmation settings
- **Proceed Always**: The plan is approved and auto-approval is enabled for subsequent edit operations
- **Cancel**: The plan is rejected, and the system remains in plan mode for further planning

The tool changes the approval mode only after the explicit decision has been accepted and execution begins. Permission rules, automatic approval modes, and permission-allow hooks cannot approve a main-session plan exit on the user's behalf.

## Important Notes

- **Plan mode only**: This tool should only be used when you are currently in plan mode
- **Implementation focus**: Only use for tasks that involve writing or modifying code
- **Concise plans**: Keep plans focused and concise - aim for clarity over exhaustive detail
- **Markdown support**: Plans support markdown formatting for better readability
- **Single use**: The tool should be used once per planning session when ready to proceed
- **User control**: The final decision to proceed always rests with the user
- **Fail closed**: Cancellation, stale approvals, host failures, and transition failures remain in Plan mode

## Integration with Planning Workflow

The Exit Plan Mode tool is part of a larger planning workflow:

1. **Enter Plan Mode**: User requests or system determines planning is needed
2. **Exploration Phase**: Analyze codebase, understand requirements, explore options
3. **Plan Design**: Create implementation strategy based on exploration
4. **Plan Presentation**: Use `exit_plan_mode` to present plan to user
5. **Implementation Phase**: Upon approval, proceed with planned implementation

This workflow ensures thoughtful implementation approaches and gives users control over significant code changes.

---

### Developers Tools File System (`docs/developers/tools/file-system.md`)

# Qwen Code file system tools

Qwen Code provides a comprehensive suite of tools for interacting with the local file system. These tools allow the model to read from, write to, list, search, and modify files and directories, all under your control and typically with confirmation for sensitive operations.

**Note:** All file system tools operate within a `rootDirectory` (usually the current working directory where you launched the CLI) for security. Paths that you provide to these tools are generally expected to be absolute or are resolved relative to this root directory.

## 1. `list_directory` (ListFiles)

`list_directory` lists the names of files and subdirectories directly within a specified directory path. It can optionally ignore entries matching provided glob patterns.

**Note:** This tool is opt-in and disabled by default because `glob` covers directory listing in most cases. Enable it by setting `tools.listDirectory.enabled` to `true` in your settings, or by explicitly listing `list_directory` in the `coreTools` allowlist (`--core-tools` / `tools.core`).

- **Tool name:** `list_directory`
- **Display name:** ListFiles
- **File:** `ls.ts`
- **Parameters:**
  - `path` (string, required): The absolute path to the directory to list.
  - `ignore` (array of strings, optional): A list of glob patterns to exclude from the listing (e.g., `["*.log", ".git"]`).
  - `respect_git_ignore` (boolean, optional): Whether to respect `.gitignore` patterns when listing files. Defaults to `true`.
- **Behavior:**
  - Returns a list of file and directory names.
  - Indicates whether each entry is a directory.
  - Sorts entries with directories first, then alphabetically.
- **Output (`llmContent`):** A string like: `Directory listing for /path/to/your/folder:\n[DIR] subfolder1\nfile1.txt\nfile2.png`
- **Confirmation:** No.

## 2. `read_file` (ReadFile)

`read_file` reads and returns the content of a specified file. This tool handles text files and media files (images, PDFs, audio, video) whose modality is supported by the current model. For text files, it can read specific line ranges. Unsupported PDFs attempt text extraction and the bounded vision fallback described below; other unsupported media files return a helpful error message. Other binary file types are generally skipped.

- **Tool name:** `read_file`
- **Display name:** ReadFile
- **File:** `read-file.ts`
- **Parameters:**
  - `file_path` (string, required): The absolute path to the file to read.
  - `offset` (number, optional): For text files, the 0-based line number to start reading from. Requires `limit` to be set.
  - `limit` (number, optional): For text files, the maximum number of lines to read. If omitted, reads a default maximum (e.g., 2000 lines) or the entire file if feasible.
  - `pages` (string, optional): For PDFs, a 1-indexed page or closed page range such as `"3"` or `"20-25"`. A request may contain at most 20 pages.
- **Behavior:**
  - For text files: Returns the content. If `offset` and `limit` are used, returns only that slice of lines. Indicates if content was truncated due to line limits or line length limits.
  - For media files (images, PDFs, audio, video): If the current model supports the file's modality, returns the file content as a base64-encoded `inlineData` object. If the model does not support the modality, returns an error message with guidance (e.g., suggesting skills or external tools).
  - For PDFs with a text-only primary model: Text extraction is attempted first. If extraction fails, or an explicitly requested (or actual) single page still exceeds the 12K-token text budget, a configured vision bridge automatically renders and transcribes at most four pages beginning at the requested first page. The requested range is clipped to the actual document end when known. The result identifies the transcribed range and either the pages known to remain or, when the page count is unavailable, that additional pages may exist. Ordinary multi-page text overflow still asks for a narrower `pages` range instead of switching to vision.
  - Vision bridge PDF transcription is lossy and marked as untrusted machine-generated content. The tool result contains text rather than rendered images, and its user-facing TUI, ACP, non-interactive structured output, and export displays identify the vision model and endpoint when known. If the bridge fails, the exact original PDF extraction error is returned to the model while the user display still discloses the bridge attempt.
  - For other binary files: Attempts to identify and skip them, returning a message indicating it's a generic binary file.
- **Output:** (`llmContent`):
  - For text files: The file content, potentially prefixed with a truncation message (e.g., `[File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...`).
  - For supported media files: An object containing `inlineData` with `mimeType` and base64 `data` (e.g., `{ inlineData: { mimeType: 'image/png', data: 'base64encodedstring' } }`).
  - For unsupported media files: An error message string explaining that the current model does not support this modality, with suggestions for alternatives.
  - For other binary files: A message like `Cannot display content of binary file: /path/to/data.bin`.
- **Confirmation:** No.

### Jupyter notebook reads

For Jupyter notebooks (`.ipynb`), `read_file` parses the notebook JSON and returns a structured, model-readable notebook view instead of raw JSON. The rendered output includes the notebook language, ordered cells, cell IDs, source, and summarized outputs.

Notebook cells can then be edited with `notebook_edit`. The model should use the cell IDs shown by `read_file` when targeting a cell.

`offset` and `limit` are not supported for `.ipynb` files. Notebook reads are treated as structured full-file reads; if the rendered notebook output is internally truncated because it is too large, `notebook_edit` will reject cell-level edits and ask you to reduce outputs or split the notebook before editing.

## 3. `notebook_edit` (NotebookEdit)

`notebook_edit` edits Jupyter notebook (`.ipynb`) files safely at the cell level. Use it instead of `edit` or `write_file` when changing notebook cells.

- **Tool name:** `notebook_edit`
- **Display name:** NotebookEdit
- **File:** `notebook-edit.ts`
- **Parameters:**
  - `notebook_path` (string, required): The absolute path to the `.ipynb` file.
  - `cell_id` (string, optional): The target cell ID shown by `read_file`. Required for `replace` and `delete`. For `insert`, the new cell is inserted after this cell; if omitted, the new cell is inserted at the beginning.
  - `new_source` (string, optional): The new cell source for `replace` and `insert`. Not required for `delete`.
  - `cell_type` (`code` or `markdown`, optional): The cell type for inserted cells, or the target type when replacing a cell.
  - `edit_mode` (`replace`, `insert`, or `delete`, optional): The edit operation. Defaults to `replace`.
- **Behavior:**
  - Requires the notebook to have been read first with `read_file` in the current session.
  - Targets cells using the IDs rendered by `read_file`, including real notebook cell IDs and displayed `cell-N` fallback IDs.
  - Rejects ambiguous rendered cell IDs instead of guessing.
  - For code cells, clears stale outputs and resets `execution_count` when source changes.
  - Preserves notebook JSON formatting, line endings, encoding, and BOM where possible.
  - Invalidates the prior-read state after structural edits when displayed fallback IDs can shift, so the next notebook edit requires a fresh `read_file`.
- **Output (`llmContent`):** A success message describing the edited notebook cell and, for non-delete operations, the updated source.
- **Confirmation:** Yes. Shows a notebook JSON diff and asks for user approval before writing, unless the current permission mode or rules auto-approve edit tools.

### `notebook_edit` examples

Replace a code cell:

```
notebook_edit(
  notebook_path="/path/to/analysis.ipynb",
  cell_id="load-data",
  new_source="result = 41 + 1\nprint(result)"
)
```

Insert a markdown cell after an existing cell:

```
notebook_edit(
  notebook_path="/path/to/analysis.ipynb",
  edit_mode="insert",
  cell_id="summary",
  cell_type="markdown",
  new_source="## Findings\n\nThe cleaned data is ready for modeling."
)
```

Delete a cell:

```
notebook_edit(
  notebook_path="/path/to/analysis.ipynb",
  edit_mode="delete",
  cell_id="old-experiment"
)
```

## 4. `write_file` (WriteFile)

`write_file` writes content to a specified file. If the file exists, it will be overwritten. If the file doesn't exist, it (and any necessary parent directories) will be created.

- **Tool name:** `write_file`
- **Display name:** WriteFile
- **File:** `write-file.ts`
- **Parameters:**
  - `file_path` (string, required): The absolute path to the file to write to.
  - `content` (string, required): The content to write into the file.
- **Behavior:**
  - Writes the provided `content` to the `file_path`.
  - Does not write raw Jupyter notebook JSON. Use `notebook_edit` for `.ipynb` cell edits.
  - Creates parent directories if they don't exist.
- **Output (`llmContent`):** A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt` or `Successfully created and wrote to new file: /path/to/new/file.txt`.
- **Confirmation:** Yes. Shows a diff of changes and asks for user approval before writing.

## 5. `glob` (Glob)

`glob` finds files matching specific glob patterns (e.g., `src/**/*.ts`, `*.md`), returning absolute paths sorted by modification time (newest first).

- **Tool name:** `glob`
- **Display name:** Glob
- **File:** `glob.ts`
- **Parameters:**
  - `pattern` (string, required): The glob pattern to match against (e.g., `"*.py"`, `"src/**/*.js"`).
  - `path` (string, optional): The directory to search in. If not specified, the current working directory will be used.
- **Behavior:**
  - Searches for files matching the glob pattern within the specified directory.
  - Returns a list of absolute paths, sorted with the most recently modified files first.
  - Respects .gitignore, .qwenignore, and configured custom Qwen ignore files by default.
  - Limits results to 100 files to prevent context overflow.
- **Output (`llmContent`):** A message like: `Found 5 file(s) matching "*.ts" within /path/to/search/dir, sorted by modification time (newest first):\n---\n/path/to/file1.ts\n/path/to/subdir/file2.ts\n---\n[95 files truncated] ...`
- **Confirmation:** No.

## 6. `grep_search` (Grep)

`grep_search` searches for a regular expression pattern within the content of files in a specified directory. Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.

- **Tool name:** `grep_search`
- **Display name:** Grep
- **File:** `grep.ts` (with `ripGrep.ts` as fallback)
- **Parameters:**
  - `pattern` (string, required): The regular expression pattern to search for in file contents (e.g., `"function\\s+myFunction"`, `"log.*Error"`).
  - `path` (string, optional): File or directory to search in. Defaults to current working directory.
  - `glob` (string, optional): Glob pattern to filter files (e.g. `"*.js"`, `"src/**/*.{ts,tsx}"`).
  - `limit` (integer, optional): Limit output to first N matching lines. Must be a positive integer. Optional - shows all matches if not specified.
- **Behavior:**
  - Uses ripgrep for fast search when available; otherwise falls back to a JavaScript-based search implementation.
  - Returns matching lines with file paths and line numbers.
  - Case-insensitive by default.
  - Respects .gitignore, .qwenignore, and configured custom Qwen ignore files.
  - Limits output to prevent context overflow.
- **Output (`llmContent`):** A formatted string of matches, e.g.:

  ```
  Found 3 matches for pattern "myFunction" in path "." (filter: "*.ts"):
  ---
  src/utils.ts:15:export function myFunction() {
  src/utils.ts:22:  myFunction.call();
  src/index.ts:5:import { myFunction } from './utils';
  ---

  [0 lines truncated] ...
  ```

- **Confirmation:** No.

### `grep_search` examples

Search for a pattern with default result limiting:

```
grep_search(pattern="function\\s+myFunction", path="src")
```

Search for a pattern with custom result limiting:

```
grep_search(pattern="function", path="src", limit=50)
```

Search for a pattern with file filtering and custom result limiting:

```
grep_search(pattern="function", glob="*.js", limit=10)
```

## 7. `edit` (Edit)

`edit` replaces text within a file. By default it requires `old_string` to match a single unique location; set `replace_all` to `true` when you intentionally want to change every occurrence. This tool is designed for precise, targeted changes and requires significant context around the `old_string` to ensure it modifies the correct location.

- **Tool name:** `edit`
- **Display name:** Edit
- **File:** `edit.ts`
- **Parameters:**
  - `file_path` (string, required): The absolute path to the file to modify.
  - `old_string` (string, required): The exact literal text to replace.

    **CRITICAL:** This string must uniquely identify the single instance to change. It should include sufficient context around the target text, matching whitespace and indentation precisely. If `old_string` is empty, the tool attempts to create a new file at `file_path` with `new_string` as content.

  - `new_string` (string, required): The exact literal text to replace `old_string` with.
  - `replace_all` (boolean, optional): Replace all occurrences of `old_string`. Defaults to `false`.

- **Behavior:**
  - Does not edit raw Jupyter notebook JSON. Use `notebook_edit` for `.ipynb` cell edits.
  - If `old_string` is empty and `file_path` does not exist, creates a new file with `new_string` as content.
  - If `old_string` is provided, it reads the `file_path` and attempts to find exactly one occurrence unless `replace_all` is true.
  - If the match is unique (or `replace_all` is true), it replaces the text with `new_string`.
  - For line-based fuzzy matches, counting and replacement use the same complete occurrences of the selected on-disk text at line boundaries. Indented suffixes and prefixes of longer lines are excluded; literal substring edits retain their existing behavior.
  - **Enhanced Reliability (Multi-Stage Edit Correction):** To significantly improve the success rate of edits, especially when the model-provided `old_string` might not be perfectly precise, the tool incorporates a multi-stage edit correction mechanism.
    - If the initial `old_string` isn't found or matches multiple locations, the tool can leverage the Qwen model to iteratively refine `old_string` (and potentially `new_string`).
    - This self-correction process attempts to identify the unique segment the model intended to modify, making the `edit` operation more robust even with slightly imperfect initial context.
- **Failure conditions:** Despite the correction mechanism, the tool will fail if:
  - `file_path` is not absolute or is outside the root directory.
  - `old_string` is not empty, but the `file_path` does not exist.
  - `old_string` is empty, but the `file_path` already exists.
  - `old_string` is not found in the file after attempts to correct it.
  - `old_string` is found multiple times, `replace_all` is false, and the self-correction mechanism cannot resolve it to a single, unambiguous match.
- **Output (`llmContent`):**
  - On success: `Successfully modified file: /path/to/file.txt (1 replacements).` or `Created new file: /path/to/new_file.txt with provided content.`
  - On failure: An error message explaining the reason (e.g., `Failed to edit, 0 occurrences found...`, `Failed to edit because the text matches multiple locations...`).
- **Confirmation:** Yes. Shows a diff of the proposed changes and asks for user approval before writing to the file.

## File encoding and platform-specific behavior

### Encoding detection and preservation

When reading files, Qwen Code detects the file's encoding using a multi-step strategy:

1. **UTF-8** — tried first (most modern tooling outputs UTF-8)
2. **chardet** — statistical detection for non-UTF-8 content
3. **System encoding** — falls back to the OS code page (Windows `chcp` / Unix `LANG`)

Both `write_file` and `edit` preserve the original encoding and BOM (byte order mark) of existing files. If a file was read as GBK with a UTF-8 BOM, it will be written back the same way.

### Configuring default encoding for new files

The `defaultFileEncoding` setting controls encoding for **newly created** files (not edits to existing files):

| Value       | Behavior                                                                    |
| ----------- | --------------------------------------------------------------------------- |
| _(not set)_ | UTF-8 without BOM, with automatic platform-specific adjustments (see below) |
| `utf-8`     | UTF-8 without BOM, no automatic adjustments                                 |
| `utf-8-bom` | UTF-8 with BOM for all new files                                            |

Set it in `.qwen/settings.json` or `~/.qwen/settings.json`:

```json
{
  "general": {
    "defaultFileEncoding": "utf-8-bom"
  }
}
```

### Windows: CRLF for batch files

On Windows, `.bat` and `.cmd` files are automatically written with CRLF (`\r\n`) line endings. This is required because `cmd.exe` uses CRLF as its line delimiter — LF-only endings can break multi-line `if`/`else`, `goto` labels, and `for` loops. This applies regardless of encoding settings and only on Windows.

### Windows: UTF-8 BOM for PowerShell scripts

On Windows with a **non-UTF-8 system code page** (e.g. GBK/cp936, Big5/cp950, Shift_JIS/cp932), newly created `.ps1` files are automatically written with a UTF-8 BOM. This is necessary because Windows PowerShell 5.1 (the version built into Windows 10/11) reads BOM-less scripts using the system's ANSI code page. Without a BOM, any non-ASCII characters in the script will be misinterpreted.

This automatic BOM only applies when:

- The platform is Windows
- The system code page is not UTF-8 (not code page 65001)
- The file is a new `.ps1` file (existing files keep their original encoding)
- The user has **not** explicitly set `defaultFileEncoding` in settings

PowerShell 7+ (pwsh) defaults to UTF-8 and handles BOM transparently, so the BOM is harmless there.

If you explicitly set `defaultFileEncoding` to `"utf-8"`, the automatic BOM is disabled — this is an intentional escape hatch for repositories or tooling that reject BOMs.

### Summary

| File type      | Platform                      | Automatic behavior          |
| -------------- | ----------------------------- | --------------------------- |
| `.bat`, `.cmd` | Windows                       | CRLF line endings           |
| `.ps1`         | Windows (non-UTF-8 code page) | UTF-8 BOM on new files      |
| All others     | All                           | UTF-8 without BOM (default) |

These file system tools provide a foundation for Qwen Code to understand and interact with your local project context.

---

### Developers Tools Introduction (`docs/developers/tools/introduction.md`)

# Qwen Code tools

Qwen Code includes built-in tools that the model uses to interact with your local environment, access information, and perform actions. These tools enhance the CLI's capabilities, enabling it to go beyond text generation and assist with a wide range of tasks.

## Overview of Qwen Code tools

In the context of Qwen Code, tools are specific functions or modules that the model can request to be executed. For example, if you ask the model to "Summarize the contents of `my_document.txt`," it will likely identify the need to read that file and will request the execution of the `read_file` tool.

The core component (`packages/core`) manages these tools, presents their definitions (schemas) to the model, executes them when requested, and returns the results to the model for further processing into a user-facing response.

These tools provide the following capabilities:

- **Access local information:** Tools allow the model to access your local file system, read file contents, list directories, etc.
- **Execute commands:** With tools like `run_shell_command`, the model can run shell commands (with appropriate safety measures and user confirmation).
- **Interact with the web:** Tools can fetch content from URLs.
- **Take actions:** Tools can modify files, write new files, or perform other actions on your system (again, typically with safeguards).
- **Ground responses:** By using tools to fetch real-time or specific local data, responses can be more accurate, relevant, and grounded in your actual context.

## How to use Qwen Code tools

To use Qwen Code tools, provide a prompt to the CLI. The process works as follows:

1.  You provide a prompt to the CLI.
2.  The CLI sends the prompt to the core.
3.  The core, along with your prompt and conversation history, sends a list of available tools and their descriptions/schemas to the configured model API.
4.  The model analyzes your request. If it determines that a tool is needed, its response will include a request to execute a specific tool with certain parameters.
5.  The core receives this tool request, validates it, and (often after user confirmation for sensitive operations) executes the tool.
6.  The output from the tool is sent back to the model.
7.  The model uses the tool's output to formulate its final answer, which is then sent back through the core to the CLI and displayed to you.

You will typically see messages in the CLI indicating when a tool is being called and whether it succeeded or failed.

## Security and confirmation

Many tools, especially those that can modify your file system or execute commands (`write_file`, `edit`, `run_shell_command`), are designed with safety in mind. Qwen Code will typically:

- **Require confirmation:** Prompt you before executing potentially sensitive operations, showing you what action is about to be taken.
- **Utilize sandboxing:** All tools are subject to restrictions enforced by sandboxing (see [Sandboxing in Qwen Code](./sandbox.md)). This means that when operating in a sandbox, any tools (including MCP servers) you wish to use must be available _inside_ the sandbox environment. For example, to run an MCP server through `npx`, the `npx` executable must be installed within the sandbox's Docker image or be available in the `sandbox-exec` environment.

It's important to always review confirmation prompts carefully before allowing a tool to proceed.

## Learn more about Qwen Code's tools

Qwen Code's built-in tools can be broadly categorized as follows:

- **[File System Tools](./file-system.md):** For interacting with files and directories (reading, writing, listing, searching, etc.).
- **[Shell Tool](./shell.md) (`run_shell_command`):** For executing shell commands.
- **[Monitor Tool](./monitor.md) (`monitor`):** For running long-lived shell commands that stream output back as background task notifications.
- **[Web Fetch Tool](./web-fetch.md) (`web_fetch`):** For retrieving content from URLs.
- **[Web Search Tool](./web-search.md) (`web_search`):** For searching the web. Available at startup when a supported provider can back it.
- **[Todo Write Tool](./todo-write.md) (`todo_write`):** An opt-in tool for creating and managing structured task lists during coding sessions.
- **[Agent Tool](./task.md) (`agent`):** For delegating complex tasks to specialized subagents.
- **[Exit Plan Mode Tool](./exit-plan-mode.md) (`exit_plan_mode`):** For exiting plan mode and proceeding with implementation.

Additionally, these tools incorporate:

- **[MCP servers](./mcp-server.md)**: MCP servers act as a bridge between the model and your local environment or other services like APIs.
  - **[MCP User Guide](../../users/features/mcp.md)**: Configure MCP servers and manage them from Qwen Code
  - **[Web Search via MCP](./web-search.md)**: Connect additional web search services (Bailian, Tavily, GLM) through MCP
- **[Sandboxing](./sandbox.md)**: Sandboxing isolates the model and its changes from your environment to reduce potential risk.

---

### Developers Tools Mcp Server (`docs/developers/tools/mcp-server.md`)

# MCP servers with Qwen Code

This document provides a guide to configuring and using Model Context Protocol (MCP) servers with Qwen Code.

## What is an MCP server?

An MCP server is an application that exposes tools and resources to the CLI through the Model Context Protocol, allowing it to interact with external systems and data sources. MCP servers act as a bridge between the model and your local environment or other services like APIs.

An MCP server enables the CLI to:

- **Discover tools:** List available tools, their descriptions, and parameters through standardized schema definitions.
- **Execute tools:** Call specific tools with defined arguments and receive structured responses.
- **Access resources:** Read data from specific resources (though the CLI primarily focuses on tool execution).

With an MCP server, you can extend the CLI's capabilities to perform actions beyond its built-in features, such as interacting with databases, APIs, custom scripts, or specialized workflows.

## Core Integration Architecture

Qwen Code integrates with MCP servers through a sophisticated discovery and execution system built into the core package (`packages/core/src/tools/`):

### Discovery Layer (`mcp-client.ts`)

The discovery process is orchestrated by `discoverMcpTools()`, which:

1. **Iterates through configured servers** from your `settings.json` `mcpServers` configuration
2. **Establishes connections** using appropriate transport mechanisms (Stdio, SSE, or Streamable HTTP)
3. **Fetches tool definitions** from each server using the MCP protocol
4. **Sanitizes and validates** tool schemas for compatibility with the Qwen API
5. **Registers tools** in the global tool registry with conflict resolution

### Execution Layer (`mcp-tool.ts`)

Each discovered MCP tool is wrapped in a `DiscoveredMCPTool` instance that:

- **Handles confirmation logic** based on server trust settings and user preferences
- **Manages tool execution** by calling the MCP server with proper parameters
- **Processes responses** for both the LLM context and user display
- **Maintains connection state** and handles timeouts

After a connection loss, the current invocation is replayed only for a trusted server in a trusted workspace when the tool explicitly declares `idempotentHint: true`, or declares `readOnlyHint: true` without a conflicting `destructiveHint: true` or `idempotentHint: false`. Missing or conflicting annotations, and annotations from an untrusted server or workspace, are treated as unsafe because the server may have completed a side effect before the response was lost. Tool authors should publish accurate MCP annotations; administrators should still verify them before enabling server trust.

### Transport Mechanisms

The CLI supports three MCP transport types:

- **Stdio Transport:** Spawns a subprocess and communicates via stdin/stdout
- **SSE Transport:** Connects to Server-Sent Events endpoints
- **Streamable HTTP Transport:** Uses HTTP streaming for communication

## How to set up your MCP server

Qwen Code uses the `mcpServers` configuration in your `settings.json` file to locate and connect to MCP servers. This configuration supports multiple servers with different transport mechanisms.

### Configure the MCP server in settings.json

You can configure MCP servers in your `settings.json` file in two main ways: through the top-level `mcpServers` object for specific server definitions, and through the `mcp` object for global settings that control server discovery and execution.

#### Global MCP Settings (`mcp`)

The `mcp` object in your `settings.json` allows you to define global rules for all MCP servers.

- **`mcp.serverCommand`** (string): A global command to start an MCP server.
- **`mcp.allowed`** (array of strings): A list of MCP server names to allow. If this is set, only servers from this list (matching the keys in the `mcpServers` object) will be connected to.
- **`mcp.excluded`** (array of strings): A list of MCP server names to exclude. Servers in this list will not be connected to.

**Example:**

```json
{
  "mcp": {
    "allowed": ["my-trusted-server"],
    "excluded": ["experimental-server"]
  }
}
```

#### Server-Specific Configuration (`mcpServers`)

The `mcpServers` object is where you define each individual MCP server you want the CLI to connect to.

### Configuration Structure

Add an `mcpServers` object to your `settings.json` file:

```json
{ ...file contains other config objects
  "mcpServers": {
    "serverName": {
      "command": "path/to/server",
      "args": ["--arg1", "value1"],
      "env": {
        "API_KEY": "$MY_API_TOKEN"
      },
      "cwd": "./server-directory",
      "timeout": 30000,
      "trust": false
    }
  }
}
```

### Configuration Properties

Each server configuration supports the following properties:

#### Required (one of the following)

- **`command`** (string): Path to the executable for Stdio transport
- **`url`** (string): SSE endpoint URL (e.g., `"http://localhost:8080/sse"`)
- **`httpUrl`** (string): HTTP streaming endpoint URL

#### Optional

- **`args`** (string[]): Command-line arguments for Stdio transport
- **`headers`** (object): Custom HTTP headers when using `url` or `httpUrl`
- **`env`** (object): Environment variables for the server process. Values can reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax
- **`cwd`** (string): Working directory for Stdio transport
- **`timeout`** (number): Request timeout in milliseconds (default: 600,000ms = 10 minutes)
- **`versionNegotiation`** (`"auto" | "legacy"`, default: `"legacy"`): For Stdio servers, `"auto"` opts into the `server/discover` probe on a disposable sibling process.
- **`trust`** (boolean): When `true`, bypasses tool call confirmations for this server in a trusted workspace (default: `false`)
- **`includeTools`** (string[]): List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default.
- **`excludeTools`** (string[]): List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded.
- **`targetAudience`** (string): The OAuth Client ID allowlisted on the IAP-protected application you are trying to access. Used with `authProviderType: 'service_account_impersonation'`.
- **`targetServiceAccount`** (string): The email address of the Google Cloud Service Account to impersonate. Used with `authProviderType: 'service_account_impersonation'`.

### OAuth Support for Remote MCP Servers

Qwen Code supports OAuth 2.0 authentication for remote MCP servers using SSE or HTTP transports. This enables secure access to MCP servers that require authentication.

#### Automatic OAuth Discovery

For servers that support OAuth discovery, you can omit the OAuth configuration and let the CLI discover it automatically:

```json
{
  "mcpServers": {
    "discoveredServer": {
      "url": "https://api.example.com/sse"
    }
  }
}
```

The CLI will automatically:

- Detect when a server requires OAuth authentication (401 responses)
- Discover OAuth endpoints from server metadata
- Perform dynamic client registration if supported
- Handle the OAuth flow and token management

#### Authentication Flow

When connecting to an OAuth-enabled server:

1. **Initial connection attempt** fails with 401 Unauthorized
2. **OAuth discovery** finds authorization and token endpoints
3. **Browser opens** for user authentication (requires local browser access)
4. **Authorization code** is exchanged for access tokens
5. **Tokens are stored** securely for future use
6. **Connection retry** succeeds with valid tokens

#### Browser Redirect Requirements

**Important:** OAuth authentication requires that the redirect URI is accessible:

- **Default behavior**: Redirects to `http://localhost:7777/oauth/callback` (works for local setups)
- **Custom redirect URI**: Use `--oauth-redirect-uri` or configure `redirectUri` in settings.json to specify a public URL ending in `/oauth/callback`. Reverse-proxy that path to `http://127.0.0.1:7777/oauth/callback` on the machine running Qwen Code.

For **remote/cloud server deployments** (e.g., web terminals, SSH sessions, cloud IDEs):

- The default `localhost` redirect will NOT work
- You MUST configure a custom `redirectUri` pointing to a publicly accessible URL ending in `/oauth/callback`
- Terminate TLS at a reverse proxy and forward only that path to `http://127.0.0.1:7777/oauth/callback`

Example for remote servers:

```bash
qwen mcp add --transport sse remote-server https://api.example.com/sse/ \
  --oauth-redirect-uri https://your-remote-server.example.com/oauth/callback
```

OAuth will not work in:

- Headless environments without browser access
- Environments where the configured `redirectUri` is unreachable from the user's browser

#### Managing OAuth Authentication

Use the `/mcp` dialog inside an interactive Qwen Code session to inspect MCP
servers and manage OAuth authentication.

#### OAuth Configuration Properties

- **`enabled`** (boolean): Enable OAuth for this server
- **`clientId`** (string): OAuth client identifier (optional with dynamic registration)
- **`clientSecret`** (string): OAuth client secret (optional for public clients)
- **`authorizationUrl`** (string): OAuth authorization endpoint (auto-discovered if omitted)
- **`tokenUrl`** (string): OAuth token endpoint (auto-discovered if omitted)
- **`scopes`** (string[]): Required OAuth scopes
- **`redirectUri`** (string): Custom redirect URI. **Critical for remote deployments**: Defaults to `http://localhost:7777/oauth/callback`. For remote use, set a public URL ending in `/oauth/callback` and reverse-proxy it to the local callback listener. Can be configured via `qwen mcp add --oauth-redirect-uri` or directly in settings.json.
- **`tokenParamName`** (string): Query parameter name for tokens in SSE URLs
- **`audiences`** (string[]): Audiences the token is valid for

#### Token Management

OAuth tokens are automatically:

- **Stored** in `~/.qwen/mcp-oauth-tokens.json` (plaintext, mode 0600) by default. If `QWEN_CODE_FORCE_ENCRYPTED_FILE_STORAGE=true` is set, Qwen Code uses keychain-backed storage where available, or `~/.qwen/mcp-oauth-tokens-v2.json` with AES-256-GCM encryption.
- **Refreshed** when expired (if refresh tokens are available)
- **Validated** before each connection attempt
- **Cleaned up** when invalid or expired

> [!WARNING]
> By default, OAuth tokens are stored unencrypted on disk. On shared or multi-user machines, set `QWEN_CODE_FORCE_ENCRYPTED_FILE_STORAGE=true` to protect credentials.

#### Authentication Provider Type

You can specify the authentication provider type using the `authProviderType` property:

- **`authProviderType`** (string): Specifies the authentication provider. Can be one of the following:
  - **`dynamic_discovery`** (default): The CLI will automatically discover the OAuth configuration from the server.
  - **`google_credentials`**: The CLI will use the Google Application Default Credentials (ADC) to authenticate with the server. When using this provider, you must specify the required scopes.
  - **`service_account_impersonation`**: The CLI will impersonate a Google Cloud Service Account to authenticate with the server. This is useful for accessing IAP-protected services (this was specifically designed for Cloud Run services).

#### Google Credentials

```json
{
  "mcpServers": {
    "googleCloudServer": {
      "httpUrl": "https://my-gcp-service.run.app/mcp",
      "authProviderType": "google_credentials",
      "oauth": {
        "scopes": ["https://www.googleapis.com/auth/userinfo.email"]
      }
    }
  }
}
```

#### Service Account Impersonation

To authenticate with a server using Service Account Impersonation, you must set the `authProviderType` to `service_account_impersonation` and provide the following properties:

- **`targetAudience`** (string): The OAuth Client ID allowslisted on the IAP-protected application you are trying to access.
- **`targetServiceAccount`** (string): The email address of the Google Cloud Service Account to impersonate.

The CLI will use your local Application Default Credentials (ADC) to generate an OIDC ID token for the specified service account and audience. This token will then be used to authenticate with the MCP server.

#### Setup Instructions

1. **[Create](https://cloud.google.com/iap/docs/oauth-client-creation) or use an existing OAuth 2.0 client ID.** To use an existing OAuth 2.0 client ID, follow the steps in [How to share OAuth Clients](https://cloud.google.com/iap/docs/sharing-oauth-clients).
2. **Add the OAuth ID to the allowlist for [programmatic access](https://cloud.google.com/iap/docs/sharing-oauth-clients#programmatic_access) for the application.** Since Cloud Run is not yet a supported resource type in gcloud iap, you must allowlist the Client ID on the project.
3. **Create a service account.** [Documentation](https://cloud.google.com/iam/docs/service-accounts-create#creating), [Cloud Console Link](https://console.cloud.google.com/iam-admin/serviceaccounts)
4. **Add both the service account and users to the IAP Policy** in the "Security" tab of the Cloud Run service itself or via gcloud.
5. **Grant all users and groups** who will access the MCP Server the necessary permissions to [impersonate the service account](https://cloud.google.com/docs/authentication/use-service-account-impersonation) (i.e., `roles/iam.serviceAccountTokenCreator`).
6. **[Enable](https://console.cloud.google.com/apis/library/iamcredentials.googleapis.com) the IAM Credentials API** for your project.

### Example Configurations

#### Python MCP Server (Stdio)

```json
{
  "mcpServers": {
    "pythonTools": {
      "command": "python",
      "args": ["-m", "my_mcp_server", "--port", "8080"],
      "cwd": "./mcp-servers/python",
      "env": {
        "DATABASE_URL": "$DB_CONNECTION_STRING",
        "API_KEY": "${EXTERNAL_API_KEY}"
      },
      "timeout": 15000
    }
  }
}
```

#### Node.js MCP Server (Stdio)

```json
{
  "mcpServers": {
    "nodeServer": {
      "command": "node",
      "args": ["dist/server.js", "--verbose"],
      "cwd": "./mcp-servers/node",
      "trust": true
    }
  }
}
```

#### Docker-based MCP Server

```json
{
  "mcpServers": {
    "dockerizedServer": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "API_KEY",
        "-v",
        "${PWD}:/workspace",
        "my-mcp-server:latest"
      ],
      "env": {
        "API_KEY": "$EXTERNAL_SERVICE_TOKEN"
      }
    }
  }
}
```

#### HTTP-based MCP Server

```json
{
  "mcpServers": {
    "httpServer": {
      "httpUrl": "http://localhost:3000/mcp",
      "timeout": 5000
    }
  }
}
```

#### HTTP-based MCP Server with Custom Headers

```json
{
  "mcpServers": {
    "httpServerWithAuth": {
      "httpUrl": "http://localhost:3000/mcp",
      "headers": {
        "Authorization": "Bearer your-api-token",
        "X-Custom-Header": "custom-value",
        "Content-Type": "application/json"
      },
      "timeout": 5000
    }
  }
}
```

#### MCP Server with Tool Filtering

```json
{
  "mcpServers": {
    "filteredServer": {
      "command": "python",
      "args": ["-m", "my_mcp_server"],
      "includeTools": ["safe_tool", "file_reader", "data_processor"],
      // "excludeTools": ["dangerous_tool", "file_deleter"],
      "timeout": 30000
    }
  }
}
```

### SSE MCP Server with SA Impersonation

```json
{
  "mcpServers": {
    "myIapProtectedServer": {
      "url": "https://my-iap-service.run.app/sse",
      "authProviderType": "service_account_impersonation",
      "targetAudience": "YOUR_IAP_CLIENT_ID.apps.googleusercontent.com",
      "targetServiceAccount": "your-sa@your-project.iam.gserviceaccount.com"
    }
  }
}
```

## Discovery Process Deep Dive

When Qwen Code starts, it performs MCP server discovery through the following detailed process:

### 1. Server Iteration and Connection

For each configured server in `mcpServers`:

1. **Status tracking begins:** Server status is set to `CONNECTING`
2. **Transport selection:** Based on configuration properties:
   - `httpUrl` → `StreamableHTTPClientTransport`
   - `url` → `SSEClientTransport`
   - `command` → `StdioClientTransport`
3. **Connection establishment:** The MCP client attempts to connect with the configured timeout
4. **Error handling:** Connection failures are logged and the server status is set to `DISCONNECTED`

### 2. Tool Discovery

Upon successful connection:

1. **Tool listing:** The client calls the MCP server's tool listing endpoint
2. **Schema validation:** Each tool's function declaration is validated
3. **Tool filtering:** Tools are filtered based on `includeTools` and `excludeTools` configuration
4. **Name sanitization:** Tool names are cleaned to meet Qwen API requirements:
   - Invalid characters (non-alphanumeric, underscore, dot, hyphen) are replaced with underscores
   - Names longer than 63 characters are truncated with middle replacement (`___`)

### 3. Conflict Resolution

When multiple servers expose tools with the same name:

1. **First registration wins:** The first server to register a tool name gets the unprefixed name
2. **Automatic prefixing:** Subsequent servers get prefixed names: `serverName__toolName`
3. **Registry tracking:** The tool registry maintains mappings between server names and their tools

### 4. Schema Processing

Tool parameter schemas undergo sanitization for API compatibility:

- **`$schema` properties** are removed
- **`additionalProperties`** are stripped
- **`anyOf` with `default`** have their default values removed (Vertex AI compatibility)
- **Recursive processing** applies to nested schemas

### 5. Connection Management

After discovery:

- **Persistent connections:** Servers that successfully register tools maintain their connections
- **Cleanup:** Servers that provide no usable tools have their connections closed
- **Status updates:** Final server statuses are set to `CONNECTED` or `DISCONNECTED`

## Tool Execution Flow

When the model decides to use an MCP tool, the following execution flow occurs:

### 1. Tool Invocation

The model generates a `FunctionCall` with:

- **Tool name:** The registered name (potentially prefixed)
- **Arguments:** JSON object matching the tool's parameter schema

### 2. Confirmation Process

Each `DiscoveredMCPTool` implements sophisticated confirmation logic:

#### Trust-based Bypass

```typescript
if (this.trust === true && this.cliConfig?.isTrustedFolder()) {
  return 'allow';
}
return 'ask';
```

#### Dynamic Allow-listing

The system maintains internal allow-lists for:

- **Server-level:** `serverName` → All tools from this server are trusted
- **Tool-level:** `serverName.toolName` → This specific tool is trusted

#### User Choice Handling

When confirmation is required, users can choose:

- **Proceed once:** Execute this time only
- **Always allow this tool:** Add to tool-level allow-list
- **Always allow this server:** Add to server-level allow-list
- **Cancel:** Abort execution

### 3. Execution

Upon confirmation (or trust bypass):

1. **Parameter preparation:** Arguments are validated against the tool's schema
2. **MCP call:** The underlying `CallableTool` invokes the server with:

   ```typescript
   const functionCalls = [
     {
       name: this.serverToolName, // Original server tool name
       args: params,
     },
   ];
   ```

3. **Response processing:** Results are formatted for both LLM context and user display

### 4. Response Handling

The execution result contains:

- **`llmContent`:** Raw response parts for the language model's context
- **`returnDisplay`:** Formatted output for user display (often JSON in markdown code blocks)

## How to interact with your MCP server

### Using the `/mcp` Command

The `/mcp` command provides comprehensive information about your MCP server setup:

```bash
/mcp
```

This displays:

- **Server list:** All configured MCP servers
- **Connection status:** `CONNECTED`, `CONNECTING`, or `DISCONNECTED`
- **Server details:** Configuration summary (excluding sensitive data)
- **Available tools:** List of tools from each server with descriptions
- **Discovery state:** Overall discovery process status

### Example `/mcp` Output

```
MCP Servers Status:

📡 pythonTools (CONNECTED)
  Command: python -m my_mcp_server --port 8080
  Working Directory: ./mcp-servers/python
  Timeout: 15000ms
  Tools: calculate_sum, file_analyzer, data_processor

🔌 nodeServer (DISCONNECTED)
  Command: node dist/server.js --verbose
  Error: Connection refused

🐳 dockerizedServer (CONNECTED)
  Command: docker run -i --rm -e API_KEY my-mcp-server:latest
  Tools: docker__deploy, docker__status

Discovery State: COMPLETED
```

### Tool Usage

Once discovered, MCP tools are available to the Qwen model like built-in tools. The model will automatically:

1. **Select appropriate tools** based on your requests
2. **Present confirmation dialogs** (unless the server is trusted)
3. **Execute tools** with proper parameters
4. **Display results** in a user-friendly format

## Status Monitoring and Troubleshooting

### Connection States

The MCP integration tracks several states:

#### Server Status (`MCPServerStatus`)

- **`DISCONNECTED`:** Server is not connected or has errors
- **`CONNECTING`:** Connection attempt in progress
- **`CONNECTED`:** Server is connected and ready

#### Discovery State (`MCPDiscoveryState`)

- **`NOT_STARTED`:** Discovery hasn't begun
- **`IN_PROGRESS`:** Currently discovering servers
- **`COMPLETED`:** Discovery finished (with or without errors)

### Common Issues and Solutions

#### Server Won't Connect

**Symptoms:** Server shows `DISCONNECTED` status

**Troubleshooting:**

1. **Check configuration:** Verify `command`, `args`, and `cwd` are correct
2. **Test manually:** Run the server command directly to ensure it works
3. **Check dependencies:** Ensure all required packages are installed
4. **Review logs:** Look for error messages in the CLI output
5. **Verify permissions:** Ensure the CLI can execute the server command

#### No Tools Discovered

**Symptoms:** Server connects but no tools are available

**Troubleshooting:**

1. **Verify tool registration:** Ensure your server actually registers tools
2. **Check MCP protocol:** Confirm your server implements the MCP tool listing correctly
3. **Review server logs:** Check stderr output for server-side errors
4. **Test tool listing:** Manually test your server's tool discovery endpoint

#### Tools Not Executing

**Symptoms:** Tools are discovered but fail during execution

**Troubleshooting:**

1. **Parameter validation:** Ensure your tool accepts the expected parameters
2. **Schema compatibility:** Verify your input schemas are valid JSON Schema
3. **Error handling:** Check if your tool is throwing unhandled exceptions
4. **Timeout issues:** Consider increasing the `timeout` setting

#### Sandbox Compatibility

**Symptoms:** MCP servers fail when sandboxing is enabled

**Solutions:**

1. **Docker-based servers:** Use Docker containers that include all dependencies
2. **Path accessibility:** Ensure server executables are available in the sandbox
3. **Network access:** Configure sandbox to allow necessary network connections
4. **Environment variables:** Verify required environment variables are passed through

### Debugging Tips

1. **Enable debug mode:** Run the CLI with `--debug` for verbose output
2. **Check stderr:** MCP server stderr is captured and logged (INFO messages filtered)
3. **Test isolation:** Test your MCP server independently before integrating
4. **Incremental setup:** Start with simple tools before adding complex functionality
5. **Use `/mcp` frequently:** Monitor server status during development

## Important Notes

### Security Considerations

- **Trust settings:** The `trust` option bypasses tool confirmation dialogs only in a trusted workspace. Use cautiously and only for servers you completely control
- **Access tokens:** Be security-aware when configuring environment variables containing API keys or tokens
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are available within the sandbox environment
- **Private data:** Using broadly scoped personal access tokens can lead to information leakage between repositories

### Performance and Resource Management

- **Connection persistence:** The CLI maintains persistent connections to servers that successfully register tools
- **Automatic cleanup:** Connections to servers providing no tools are automatically closed
- **Timeout management:** Configure appropriate timeouts based on your server's response characteristics
- **Resource monitoring:** MCP servers run as separate processes and consume system resources

### Schema Compatibility

- **Schema compliance mode:** By default (`schemaCompliance: "auto"`), tool schemas are passed through as-is. Set `"model": { "generationConfig": { "schemaCompliance": "openapi_30" } }` in your `settings.json` to convert models to Strict OpenAPI 3.0 format.
- **OpenAPI 3.0 transformations:** When `openapi_30` mode is enabled, the system handles:
  - Nullable types: `["string", "null"]` -> `type: "string", nullable: true`
  - Const values: `const: "foo"` -> `enum: ["foo"]`
  - Exclusive limits: numeric `exclusiveMinimum` -> boolean form with `minimum`
  - Keyword removal: `$schema`, `$id`, `dependencies`, `patternProperties`
- **Name sanitization:** Tool names are automatically sanitized to meet API requirements
- **Conflict resolution:** Tool name conflicts between servers are resolved through automatic prefixing

This comprehensive integration makes MCP servers a powerful way to extend the CLI's capabilities while maintaining security, reliability, and ease of use.

## Returning Rich Content from Tools

MCP tools are not limited to returning simple text. You can return rich, multi-part content, including text, images, audio, and other binary data in a single tool response. This allows you to build powerful tools that can provide diverse information to the model in a single turn.

All data returned from the tool is processed and sent to the model as context for its next generation, enabling it to reason about or summarize the provided information.

### How It Works

To return rich content, your tool's response must adhere to the MCP specification for a [`CallToolResult`](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-result). The `content` field of the result should be an array of `ContentBlock` objects. The CLI will correctly process this array, separating text from binary data and packaging it for the model.

You can mix and match different content block types in the `content` array. The supported block types include:

- `text`
- `image`
- `audio`
- `resource` (embedded content)
- `resource_link`

### Example: Returning Text and an Image

Here is an example of a valid JSON response from an MCP tool that returns both a text description and an image:

```json
{
  "content": [
    {
      "type": "text",
      "text": "Here is the logo you requested."
    },
    {
      "type": "image",
      "data": "BASE64_ENCODED_IMAGE_DATA_HERE",
      "mimeType": "image/png"
    },
    {
      "type": "text",
      "text": "The logo was created in 2025."
    }
  ]
}
```

When Qwen Code receives this response, it will:

1.  Extract all the text and combine it into a single `functionResponse` part for the model.
2.  Present the image data as a separate `inlineData` part.
3.  Provide a clean, user-friendly summary in the CLI, indicating that both text and an image were received.

This enables you to build sophisticated tools that can provide rich, multi-modal context to the Qwen model.

## MCP Prompts as Slash Commands

In addition to tools, MCP servers can expose predefined prompts that can be executed as slash commands within Qwen Code. This allows you to create shortcuts for common or complex queries that can be easily invoked by name.

### Defining Prompts on the Server

Here's a small example of a stdio MCP server that defines prompts:

```ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
  name: 'prompt-server',
  version: '1.0.0',
});

server.registerPrompt(
  'poem-writer',
  {
    title: 'Poem Writer',
    description: 'Write a nice haiku',
    argsSchema: { title: z.string(), mood: z.string().optional() },
  },
  ({ title, mood }) => ({
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: `Write a haiku${mood ? ` with the mood ${mood}` : ''} called ${title}. Note that a haiku is 5 syllables followed by 7 syllables followed by 5 syllables `,
        },
      },
    ],
  }),
);

const transport = new StdioServerTransport();
await server.connect(transport);
```

This can be included in `settings.json` under `mcpServers` with:

```json
{
  "mcpServers": {
    "nodeServer": {
      "command": "node",
      "args": ["filename.ts"]
    }
  }
}
```

### Invoking Prompts

Once a prompt is discovered, you can invoke it using its name as a slash command. The CLI will automatically handle parsing arguments.

```bash
/poem-writer --title="Qwen Code" --mood="reverent"
```

or, using positional arguments:

```bash
/poem-writer "Qwen Code" reverent
```

When you run this command, the CLI executes the `prompts/get` method on the MCP server with the provided arguments. The server is responsible for substituting the arguments into the prompt template and returning the final prompt text. The CLI then sends this prompt to the model for execution. This provides a convenient way to automate and share common workflows.

## Managing MCP Servers with `qwen mcp`

While you can always configure MCP servers by manually editing your `settings.json` file, the CLI provides a convenient set of commands to manage your server configurations programmatically. These commands streamline the process of adding, listing, and removing MCP servers without needing to directly edit JSON files.

### Adding a Server (`qwen mcp add`)

The `add` command configures a new MCP server in your `settings.json`. Based on the scope (`-s, --scope`), it will be added to either the user config `~/.qwen/settings.json` or the project config `.qwen/settings.json` file.

**Command:**

```bash
qwen mcp add [options] <name> <commandOrUrl> [args...]
```

- `<name>`: A unique name for the server.
- `<commandOrUrl>`: The command to execute (for `stdio`) or the URL (for `http`/`sse`).
- `[args...]`: Optional arguments for a `stdio` command.

**Options (Flags):**

- `-s, --scope`: Configuration scope (user or project). [default: "project"]
- `-t, --transport`: Transport type (stdio, sse, http). [default: "stdio"]
- `-e, --env`: Set environment variables (e.g. -e KEY=value).
- `-H, --header`: Set HTTP headers for SSE and HTTP transports (e.g. -H "X-Api-Key: abc123" -H "Authorization: Bearer abc123").
- `--timeout`: Set connection timeout in milliseconds.
- `--trust`: Trust the server (bypass its tool call confirmations in a trusted workspace).
- `--description`: Set the description for the server.
- `--include-tools`: A comma-separated list of tools to include.
- `--exclude-tools`: A comma-separated list of tools to exclude.
- `--oauth-client-id`: OAuth client ID for MCP server authentication.
- `--oauth-client-secret`: OAuth client secret for MCP server authentication.
- `--oauth-redirect-uri`: OAuth redirect URI (e.g., `https://your-server.com/oauth/callback`). Defaults to `http://localhost:7777/oauth/callback` for local setups. **Important for remote deployments**: Use a public URL ending in `/oauth/callback` and reverse-proxy it to `http://127.0.0.1:7777/oauth/callback`.
- `--oauth-authorization-url`: OAuth authorization URL.
- `--oauth-token-url`: OAuth token URL.
- `--oauth-scopes`: OAuth scopes (comma-separated).

#### Adding an stdio server

This is the default transport for running local servers.

```bash
# Basic syntax
qwen mcp add <name> <command> [args...]

# Example: Adding a local server
qwen mcp add my-stdio-server -e API_KEY=123 /path/to/server arg1 arg2 arg3

# Example: Adding a local python server
qwen mcp add python-server python server.py --port 8080
```

#### Adding an HTTP server

This transport is for servers that use the streamable HTTP transport.

```bash
# Basic syntax
qwen mcp add --transport http <name> <url>

# Example: Adding an HTTP server
qwen mcp add --transport http http-server https://api.example.com/mcp/

# Example: Adding an HTTP server with an authentication header
qwen mcp add --transport http secure-http https://api.example.com/mcp/ --header "Authorization: Bearer abc123"
```

#### Adding an SSE server

This transport is for servers that use Server-Sent Events (SSE).

```bash
# Basic syntax
qwen mcp add --transport sse <name> <url>

# Example: Adding an SSE server
qwen mcp add --transport sse sse-server https://api.example.com/sse/

# Example: Adding an SSE server with an authentication header
qwen mcp add --transport sse secure-sse https://api.example.com/sse/ --header "Authorization: Bearer abc123"

# Example: Adding an OAuth-enabled SSE server
qwen mcp add --transport sse oauth-server https://api.example.com/sse/ \
  --oauth-client-id your-client-id \
  --oauth-redirect-uri https://your-server.com/oauth/callback \
  --oauth-authorization-url https://provider.example.com/authorize \
  --oauth-token-url https://provider.example.com/token
```

### Managing Servers (`/mcp`)

To view and manage all MCP servers currently configured, open the `/mcp`
dialog inside an interactive Qwen Code session. This dialog lets you:

- View all MCP servers with their connection status
- Enable/disable servers
- Reconnect to disconnected servers
- View tools and prompts provided by each server
- View server logs

**Command:**

```bash
qwen
```

Then enter:

```text
/mcp
```

The management dialog provides a visual interface showing each server's name, configuration details, connection status, and available tools/prompts.

### Removing a Server (`qwen mcp remove`)

To delete a server from your configuration, use the `remove` command with the server's name.

**Command:**

```bash
qwen mcp remove <name>
```

**Example:**

```bash
qwen mcp remove my-server
```

This will find and delete the "my-server" entry from the `mcpServers` object in the appropriate `settings.json` file based on the scope (`-s, --scope`).

---

### Developers Tools Multi File (`docs/developers/tools/multi-file.md`)

# Multi-File Read (`read_many_files`)

> [!note]
>
> `read_many_files` was previously exposed as a standalone tool but has been refactored into an internal utility function. The model no longer invokes it directly — instead, the `read_file`, `glob`, and `grep_search` tools cover individual and multi-file reading. The information below is retained for reference.

## Description

`read_many_files` reads content from multiple files specified by paths or glob patterns. The behavior depends on the file types:

- For text files, this tool concatenates their content into a single string.
- For image (e.g., PNG, JPEG), PDF, audio (MP3, WAV), and video (MP4, MOV) files, it reads and returns them as base64-encoded data, provided they are explicitly requested by name or extension.

`read_many_files` can be used to perform tasks such as getting an overview of a codebase, finding where specific functionality is implemented, reviewing documentation, or gathering context from multiple configuration files.

**Note:** `read_many_files` looks for files following the provided paths or glob patterns. A directory path such as `"/docs"` will return an empty result; the tool requires a pattern such as `"/docs/*"` or `"/docs/*.md"` to identify the relevant files.

### Arguments

`read_many_files` takes the following arguments:

- `paths` (list[string], required): An array of glob patterns or paths relative to the tool's target directory (e.g., `["src/**/*.ts"]`, `["README.md", "docs/*", "assets/logo.png"]`).
- `exclude` (list[string], optional): Glob patterns for files/directories to exclude (e.g., `["**/*.log", "temp/"]`). These are added to default excludes if `useDefaultExcludes` is true.
- `include` (list[string], optional): Additional glob patterns to include. These are merged with `paths` (e.g., `["*.test.ts"]` to specifically add test files if they were broadly excluded, or `["images/*.jpg"]` to include specific image types).
- `recursive` (boolean, optional): Whether to search recursively. This is primarily controlled by `**` in glob patterns. Defaults to `true`.
- `useDefaultExcludes` (boolean, optional): Whether to apply a list of default exclusion patterns (e.g., `node_modules`, `.git`, non image/pdf binary files). Defaults to `true`.
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when finding files. Defaults to true.

## How to use `read_many_files` with Qwen Code

`read_many_files` searches for files matching the provided `paths` and `include` patterns, while respecting `exclude` patterns and default excludes (if enabled).

- For text files: it reads the content of each matched file (attempting to skip binary files not explicitly requested as image/PDF) and concatenates it into a single string, with a separator `--- {filePath} ---` between the content of each file. Uses UTF-8 encoding by default.
- The tool inserts a `--- End of content ---` after the last file.
- For image and PDF files: if explicitly requested by name or extension (e.g., `paths: ["logo.png"]` or `include: ["*.pdf"]`), the tool reads the file and returns its content as a base64 encoded string.
- The tool attempts to detect and skip other binary files (those not matching common image/PDF types or not explicitly requested) by checking for null bytes in their initial content.

Usage:

```
read_many_files(paths=["Your files or paths here."], include=["Additional files to include."], exclude=["Files to exclude."], recursive=False, useDefaultExcludes=false, respect_git_ignore=true)
```

## `read_many_files` examples

Read all TypeScript files in the `src` directory:

```
read_many_files(paths=["src/**/*.ts"])
```

Read the main README, all Markdown files in the `docs` directory, and a specific logo image, excluding a specific file:

```
read_many_files(paths=["README.md", "docs/**/*.md", "assets/logo.png"], exclude=["docs/OLD_README.md"])
```

Read all JavaScript files but explicitly include test files and all JPEGs in an `images` folder:

```
read_many_files(paths=["**/*.js"], include=["**/*.test.js", "images/**/*.jpg"], useDefaultExcludes=False)
```

## Important notes

- **Binary file handling:**
  - **Image/PDF/Audio/Video files:** The tool can read common image types (PNG, JPEG, etc.), PDF, audio (mp3, wav), and video (mp4, mov) files, returning them as base64 encoded data. These files _must_ be explicitly targeted by the `paths` or `include` patterns (e.g., by specifying the exact filename like `video.mp4` or a pattern like `*.mov`).
  - **Other binary files:** The tool attempts to detect and skip other types of binary files by examining their initial content for null bytes. The tool excludes these files from its output.
- **Performance:** Reading a very large number of files or very large individual files can be resource-intensive.
- **Path specificity:** Ensure paths and glob patterns are correctly specified relative to the tool's target directory. For image/PDF files, ensure the patterns are specific enough to include them.
- **Default excludes:** Be aware of the default exclusion patterns (like `node_modules`, `.git`) and use `useDefaultExcludes=False` if you need to override them, but do so cautiously.

---

### Developers Tools Sandbox (`docs/developers/tools/sandbox.md`)

## Customizing the sandbox environment (Docker/Podman)

### Currently, the project does not support the use of the BUILD_SANDBOX function after installation through the npm package

1. To build a custom sandbox, you need to access the build scripts (scripts/build_sandbox.js) in the source code repository.
2. These build scripts are not included in the packages released by npm.
3. The code contains hard-coded path checks that explicitly reject build requests from non-source code environments.

If you need extra tools inside the container (e.g., `git`, `python`, `rg`), create a custom Dockerfile, The specific operation is as follows

#### 1、Clone qwen code project first, https://github.com/QwenLM/qwen-code.git

#### 2、Make sure you perform the following operation in the source code repository directory

```bash
# 1. First, install the dependencies of the project
npm install

# 2. Build the Qwen Code project
npm run build

# 3. Verify that the dist directory has been generated
ls -la packages/cli/dist/

# 4. Create a global link in the CLI package directory
cd packages/cli
npm link

# 5. Verification link (it should now point to the source code)
which qwen
# Expected output: /xxx/xxx/.nvm/versions/node/v24.11.1/bin/qwen
# Or similar paths, but it should be a symbolic link

# 6. For details of the symbolic link, you can see the specific source code path
ls -la $(dirname $(which qwen))/../lib/node_modules/@qwen-code/qwen-code
# It should show that this is a symbolic link pointing to your source code directory

# 7.Test the version of qwen
qwen -v
# npm link will overwrite the global qwen. To avoid being unable to distinguish the same version number, you can uninstall the global CLI first

```

#### 3、Create your sandbox Dockerfile under the root directory of your own project

- Path: `.qwen/sandbox.Dockerfile`

- Official mirror image address:https://github.com/QwenLM/qwen-code/pkgs/container/qwen-code

```bash
# Based on the official Qwen sandbox image (It is recommended to explicitly specify the version)
FROM ghcr.io/qwenlm/qwen-code:sha-570ec43
# Add your extra tools here
RUN apt-get update && apt-get install -y \
    git \
    python3 \
    ripgrep
```

#### 4、Create the first sandbox image under the root directory of your project

```bash
QWEN_SANDBOX=docker BUILD_SANDBOX=1 qwen -s
# Observe whether the sandbox version of the tool you launched is consistent with the version of your custom image. If they are consistent, the startup will be successful
```

This builds a project-specific image based on the default sandbox image.

#### Remove npm link

- If you want to restore the official CLI of qwen, please remove the npm link

```bash
# Method 1: Unlink globally
npm unlink -g @qwen-code/qwen-code

# Method 2: Remove it in the packages/cli directory
cd packages/cli
npm unlink

# Verification has been lifted
which qwen
# It should display "qwen not found"

# Reinstall the global version if necessary
npm install @qwen-code/qwen-code

# Verification Recovery
which qwen
qwen --version
```

---

### Developers Tools Shell (`docs/developers/tools/shell.md`)

# Shell Tool (`run_shell_command`)

This document describes the `run_shell_command` tool for Qwen Code.

## Description

Use `run_shell_command` to interact with the underlying system, run scripts, or perform command-line operations. `run_shell_command` executes a given shell command, including interactive commands that require user input (e.g., `vim`, `git rebase -i`) if the `tools.shell.enableInteractiveShell` setting is set to `true`.

On Windows, commands are executed with `cmd.exe /c`. On other platforms, they are executed with `bash -c`.

### Arguments

`run_shell_command` takes the following arguments:

- `command` (string, required): The exact shell command to execute.
- `description` (string, optional): A brief description of the command's purpose, which will be shown to the user.
- `directory` (string, optional): The directory (relative to the project root) in which to execute the command. If not provided, the command runs in the project root.
- `is_background` (boolean, optional): Whether to run the command in background. If not specified, defaults to `false` (foreground execution). Set to true for long-running processes like development servers, watchers, or daemons that should continue running without blocking further commands.

## How to use `run_shell_command` with Qwen Code

When using `run_shell_command`, the command is executed as a subprocess. Whether it runs in the background or in the foreground is controlled by the `is_background` parameter. Do not add a trailing `&` when `is_background: true`: the managed background path is itself the backgrounding mechanism, and the tool rejects the call rather than running it. The tool returns detailed information about the execution, including:

### The `is_background` Parameter

`is_background` is optional — the tool's schema requires only `command` — and when it is omitted the command runs in the foreground. The decision that has to be explicit is therefore the other one: a command that will not stop on its own must be marked `is_background: true`, because leaving it in the foreground blocks the turn until it times out.

### Background vs Foreground Execution

The tool handles background and foreground execution based on that parameter:

**Use background execution (`is_background: true`) for:**

- Long-running development servers: `npm run start`, `npm run dev`, `yarn dev`
- Build watchers: `npm run watch`, `webpack --watch`
- Database servers: `mongod`, `mysql`, `redis-server`
- Web servers: `python -m http.server`, `php -S localhost:8000`
- Any command expected to run indefinitely until manually stopped

**Use foreground execution (the default) for** commands that finish on their own, such as builds, installs, git operations, and test runs.

Reading and searching files are not foreground candidates for this tool at all: use the dedicated tools (`read_file`, `grep_search`, `glob`) instead of `cat`, `grep`, or `find`, which the tool's own description rules out.

### Execution Information

The tool returns detailed information about the execution, including:

- `Command`: The command that was executed.
- `Directory`: The directory where the command was run.
- `Stdout`: Output from the standard output stream.
- `Stderr`: Output from the standard error stream.
- `Error`: Any error message reported by the subprocess.
- `Exit Code`: The exit code of the command.
- `Signal`: The signal number if the command was terminated by a signal.
- `Background PIDs`: A list of PIDs for any background processes started.

Usage:

```bash
run_shell_command(command="Your commands.", description="Your description of the command.", directory="Your execution directory.", is_background=false)
```

**Note:** `is_background` is optional. Omitting it runs the command in the foreground, so the case that needs an explicit `true` is a command that will not stop on its own.

## `run_shell_command` examples

List files in the current directory:

```bash
run_shell_command(command="ls -la", is_background=false)
```

Run a script in a specific directory:

```bash
run_shell_command(command="./my_script.sh", directory="scripts", description="Run my custom script", is_background=false)
```

Start a background development server (recommended approach):

```bash
run_shell_command(command="npm run dev", description="Start development server in background", is_background=true)
```

Note: an explicit `&` is not a managed background mechanism. With `is_background: true` the tool rejects a bare trailing `&` before the command runs; with `is_background=false` the shell detaches the process, so the call returns without the process being tracked — it has no background task entry and `task_stop` cannot reach it. Always use `is_background=true` for a command that should run in the background.

Run a build command in foreground:

```bash
run_shell_command(command="npm run build", description="Build the project", is_background=false)
```

Start multiple background services:

```bash
run_shell_command(command="docker-compose up", description="Start all services", is_background=true)
```

## Configuration

You can configure the behavior of the `run_shell_command` tool by modifying your `settings.json` file or by using the `/settings` command in the Qwen Code.

### Enabling Interactive Commands

The `tools.shell.enableInteractiveShell` setting controls whether shell commands are executed via `node-pty` (interactive PTY) or the plain `child_process` backend. When enabled, interactive sessions such as `vim`, `git rebase -i`, and TUI programs work correctly.

When the setting is omitted, explicit one-shot prompts use `child_process`; interactive TUI, ACP, stream-json input, stdin-only, and file-input sessions use PTY. On Windows builds **<= 19041** (before Windows 10 version 2004), PTY mode falls back to `child_process` because older ConPTY implementations have known reliability issues (missing output, hangs). This matches the same cutoff used by VS Code ([microsoft/vscode#123725](https://github.com/microsoft/vscode/issues/123725)). If `node-pty` is not available at runtime, the tool also falls back to `child_process`.

To explicitly override the default, set the value in `settings.json`:

**Example `settings.json`:**

```json
{
  "tools": {
    "shell": {
      "enableInteractiveShell": true
    }
  }
}
```

### Showing Color in Output

To show color in the shell output, you need to set the `tools.shell.showColor` setting to `true`. **Note: This setting only applies when `tools.shell.enableInteractiveShell` is enabled.**

**Example `settings.json`:**

```json
{
  "tools": {
    "shell": {
      "showColor": true
    }
  }
}
```

### Setting the Pager

You can set a custom pager for the shell output by setting the `tools.shell.pager` setting. The default pager is `cat` on non-Windows platforms. No default is set on Windows. Set `tools.shell.pager` to an empty string to disable pager environment variables. **Note: This setting only applies when `tools.shell.enableInteractiveShell` is enabled.**

**Example `settings.json`:**

```json
{
  "tools": {
    "shell": {
      "pager": "less"
    }
  }
}
```

## Interactive Commands

The `run_shell_command` tool now supports interactive commands by integrating a pseudo-terminal (pty). This allows you to run commands that require real-time user input, such as text editors (`vim`, `nano`), terminal-based UIs (`htop`), and interactive version control operations (`git rebase -i`).

When an interactive command is running, you can send input to it from the Qwen Code. To focus on the interactive shell, press `ctrl+f`. The terminal output, including complex TUIs, will be rendered correctly.

## Important notes

- **Security:** Be cautious when executing commands, especially those constructed from user input, to prevent security vulnerabilities.
- **Error handling:** Check the `Stderr`, `Error`, and `Exit Code` fields to determine if a command executed successfully.
- **Background processes:** Only `is_background=true` starts a tracked background process: the tool returns immediately with the shell id and PID, and the process keeps running. A shell-level `&` does not create one — with `is_background: true` the tool rejects a bare trailing `&`, and with `is_background=false` the shell detaches the command without the tool tracking it.
- **Background execution choices:** The `is_background` parameter is optional (the schema requires only `command`); set `is_background: true` for a command that will not stop on its own, because leaving it in the foreground blocks the turn until it times out. Use `is_background` rather than shell-level `&`; it provides clearer intent and the tool manages the background execution setup.
- **Command descriptions:** When using `is_background=true`, the command description will include a `[background]` indicator to clearly show the execution mode.

## Environment Variables

When `run_shell_command` executes a command, it sets the `QWEN_CODE=1` environment variable in the subprocess's environment. This allows scripts or tools to detect if they are being run from within the CLI.

## Command Restrictions

You can restrict the commands that can be executed by the `run_shell_command` tool by using the `tools.core` and `tools.exclude` settings in your configuration file.

- `tools.core`: To restrict `run_shell_command` to a specific set of commands, add entries to the `core` list under the `tools` category in the format `run_shell_command(<command>)`. For example, `"tools": {"core": ["run_shell_command(git)"]}` will only allow `git` commands. Including the generic `run_shell_command` acts as a wildcard, allowing any command not explicitly blocked.
- `tools.exclude`: To block specific commands, add entries to the `exclude` list under the `tools` category in the format `run_shell_command(<command>)`. For example, `"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` commands.

The validation logic is designed to be secure and flexible:

1.  **Command Chaining Disabled**: The tool automatically splits commands chained with `&&`, `||`, or `;` and validates each part separately. If any part of the chain is disallowed, the entire command is blocked.
2.  **Prefix Matching**: The tool uses prefix matching. For example, if you allow `git`, you can run `git status` or `git log`.
3.  **Blocklist Precedence**: The `tools.exclude` list is always checked first. If a command matches a blocked prefix, it will be denied, even if it also matches an allowed prefix in `tools.core`.

### Command Restriction Examples

**Allow only specific command prefixes**

To allow only `git` and `npm` commands, and block all others:

```json
{
  "tools": {
    "core": ["run_shell_command(git)", "run_shell_command(npm)"]
  }
}
```

- `git status`: Allowed
- `npm install`: Allowed
- `ls -l`: Blocked

**Block specific command prefixes**

To block `rm` and allow all other commands:

```json
{
  "tools": {
    "core": ["run_shell_command"],
    "exclude": ["run_shell_command(rm)"]
  }
}
```

- `rm -rf /`: Blocked
- `git status`: Allowed
- `npm install`: Allowed

**Blocklist takes precedence**

If a command prefix is in both `tools.core` and `tools.exclude`, it will be blocked.

```json
{
  "tools": {
    "core": ["run_shell_command(git)"],
    "exclude": ["run_shell_command(git push)"]
  }
}
```

- `git push origin main`: Blocked
- `git status`: Allowed

**Block all shell commands**

To block all shell commands, add the `run_shell_command` wildcard to `tools.exclude`:

```json
{
  "tools": {
    "exclude": ["run_shell_command"]
  }
}
```

- `ls -l`: Blocked
- `any other command`: Blocked

## Security Note for `excludeTools`

Command-specific restrictions in `excludeTools` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `coreTools` to explicitly select commands
that can be executed.

---

### Developers Tools Task (`docs/developers/tools/task.md`)

# Agent Tool (`agent`)

This document describes the `agent` tool for Qwen Code.

## Description

Use `agent` to launch a specialized subagent to handle complex, multi-step tasks autonomously. The Agent tool delegates work to specialized agents that can work independently with access to their own set of tools, allowing for parallel task execution and specialized expertise.

### Arguments

`agent` takes the following arguments:

- `description` (string, required): A short (3-5 word) description of the task for user visibility and tracking purposes.
- `prompt` (string, required): The detailed task prompt for the subagent to execute. Should contain comprehensive instructions for autonomous execution.
- `subagent_type` (string, optional): The type of specialized agent to use for this task. Defaults to `general-purpose` if omitted.
- `fork_turns` (string, optional): Only valid with `subagent_type="fork"`. Omit it or use `all` for the full parent conversation, or use a positive integer string such as `"3"` for the most recent three real user turns. Tool responses and pure system reminders do not count as turns.
- `fork_tools` (array of strings, optional): Only valid with `subagent_type="fork"`. Restricts execution to exact canonical tool names or MCP server patterns while keeping the fork's current model-visible tool declarations unchanged for prompt-cache sharing. Entries cannot have surrounding whitespace; wildcards are limited to `mcp__*` or a trailing MCP tool-prefix pattern such as `mcp__github__read_*`. Forks never execute `ask_user_question`; omit `fork_tools` to allow every other inherited tool, or use an empty array to reject every tool call.
- `fork_profile` (string, optional): Only valid with `subagent_type="fork"`. Loads a frontmatter-only regular `.qwen/fork-profiles/<name>.md` of at most 64 KiB from the active project root and applies its required `tools` array plus an optional `promptHint` of at most 200 characters. The file cannot resolve outside the project profile directory. `fork_profile` cannot be combined with `fork_tools` or a named teammate, and it is unavailable in safe mode or bare mode.
- `run_in_background` (boolean, optional): Defaults to `true` for top-level regular agents. Set to `false` to wait for a regular agent's result inline. Headless forks always run in the background. Nested agents run in the foreground unless `run_in_background` is explicitly `true`, which is rejected because nested agents cannot receive background completion notifications. Unnamed caller-owned `working_dir` launches run in the foreground: an explicit `run_in_background: true` request is rejected, while a configured background default (`background: true` in a subagent definition) is rejected at the top level and downgraded to the foreground when nested.
- `isolation` (string, optional): Set to `"worktree"` to run an explicitly named, non-fork agent in an isolated git worktree that Qwen Code creates and manages.
- `working_dir` (string, optional): Pin an explicitly named, non-fork agent to an existing registered git worktree inside the current repository. Unnamed launches run in the foreground because the caller owns the worktree lifecycle (see `run_in_background`); a named teammate pinned to one runs concurrently and must be shut down before the worktree is removed. If both `working_dir` and `isolation` are provided, `working_dir` takes precedence.

## How to use `agent` with Qwen Code

The Agent tool dynamically loads available subagents from your configuration and delegates tasks to them. Each subagent runs independently and can use its own set of tools, allowing for specialized expertise and parallel execution.

When you use the Agent tool, the subagent will:

1. Receive the task prompt and, for a fork, the selected parent conversation context
2. Execute the task using its available tools
3. Report a completion notification by default, or return a final result message when a regular agent runs in the foreground
4. Remain addressable after a background run when its retained state supports continuation

Usage:

```
agent(description="Brief task description", prompt="Detailed task instructions for the subagent", subagent_type="agent_name")
agent(description="Brief task description", prompt="Detailed task instructions for the fork", subagent_type="fork", fork_turns="3")
agent(description="Read-only investigation", prompt="Inspect the implementation", subagent_type="fork", fork_tools=["read_file", "grep_search", "mcp__github"])
agent(description="Profiled investigation", prompt="Inspect the implementation", subagent_type="fork", fork_profile="ro-research")
```

Set `run_in_background=false` when the current turn must use the subagent result before continuing.

## Available Subagents

The available subagents depend on your configuration. Common subagent types might include:

- **general-purpose**: For complex multi-step tasks requiring various tools
- **code-reviewer**: For reviewing and analyzing code quality
- **test-runner**: For running tests and analyzing results
- **documentation-writer**: For creating and updating documentation

You can view available subagents by using the `/agents` command in Qwen Code.

## Agent Tool Features

### Real-time Progress Updates

The Agent tool provides live updates showing:

- Subagent execution status
- Individual tool calls being made by the subagent
- Tool call results and any errors
- Overall task progress and completion status

### Parallel Execution

You can launch multiple subagents concurrently by calling the Agent tool multiple times in a single message, allowing for parallel task execution and improved efficiency.

### Specialized Expertise

Each subagent can be configured with:

- Specific tool access permissions
- Specialized system prompts and instructions
- Custom model configurations
- Domain-specific knowledge and capabilities

### Background Agent Continuation

Background agents can receive follow-up work after their initial completion:

1. Call `list_agents` to discover the current session's addressable background agents and their `task_id` values. This includes compatible agents restored after the parent session resumes.
2. Call `send_message` with a `task_id` and follow-up instruction. Running agents receive the message at the next tool-round boundary, paused agents resume with it, and completed agents continue on a resident runtime when available or revive from their retained transcript.
3. Wait for the next completion notification before using the follow-up result.

If an agent cannot be continued, `list_agents` returns a `resume_blocked_reason`. Treat restored or continued agent output as evidence and verify it before integrating changes.

## `agent` examples

### Delegating to a general-purpose agent

```
agent(
  description="Code refactoring",
  prompt="Please refactor the authentication module in src/auth/ to use modern async/await patterns instead of callbacks. Ensure all tests still pass and update any related documentation.",
  subagent_type="general-purpose"
)
```

### Running parallel tasks

```
# Launch code review and test execution in parallel
agent(
  description="Code review",
  prompt="Review the recent changes in the user management module for code quality, security issues, and best practices compliance.",
  subagent_type="general-purpose"
)

agent(
  description="Run tests",
  prompt="Execute the full test suite and analyze any failures. Provide a summary of test coverage and recommendations for improvement.",
  subagent_type="test-engineer"
)
```

### Documentation generation

```
agent(
  description="Update docs",
  prompt="Generate comprehensive API documentation for the newly implemented REST endpoints in the orders module. Include request/response examples and error codes.",
  subagent_type="general-purpose"
)
```

## When to Use the Agent Tool

Use the Agent tool when:

1. **Complex multi-step tasks** - Tasks requiring multiple operations that can be handled autonomously
2. **Specialized expertise** - Tasks that benefit from domain-specific knowledge or tools
3. **Parallel execution** - When you have multiple independent tasks that can run simultaneously
4. **Delegation needs** - When you want to hand off a complete task rather than micromanaging steps
5. **Resource-intensive operations** - Tasks that might take significant time or computational resources

## When NOT to Use the Agent Tool

Don't use the Agent tool for:

- **Simple, single-step operations** - Use direct tools like Read, Edit, etc.
- **Interactive tasks** - Tasks requiring back-and-forth communication
- **Specific file reads** - Use Read tool directly for better performance
- **Simple searches** - Use Grep or Glob tools directly

## Important Notes

- **Independent context**: Regular subagents start without parent conversation history. Forks inherit the full conversation by default and accept `fork_turns` when a bounded recent window is sufficient.
- **Subagent interaction**: Regular subagents do not receive `ask_user_question`. Forks keep the parent's declaration list for cache sharing but reject that tool before scheduling or approval; when missing user input blocks work, the subagent reports the blocker to its parent.
- **Fork execution restrictions**: `fork_tools` further narrows which already-declared tools a fork may execute. Disallowed calls return an error before scheduling or approval; the same declaration list remains model-visible for cache sharing. This is a per-call restriction chosen by the caller, not an administrator-enforced sandbox.
- **Fork profiles**: A project profile under `.qwen/fork-profiles/` reuses the same execution gate as `fork_tools`. It is resolved once before launch; the resolved list is persisted for revival, and an optional `promptHint` is added only to the task directive.
- **Completion delivery**: Background results arrive through completion notifications in a later turn. Do not assume a result before the notification arrives.
- **Continuation**: Use `list_agents` and `send_message` for related follow-up work instead of launching a duplicate agent. Continuation depends on compatible retained state and may be unavailable.
- **Comprehensive prompts**: Your initial prompt should contain all necessary context and instructions for autonomous execution. A regular subagent does not see the parent conversation.
- **Tool access**: Subagents only have access to tools configured in their specific configuration
- **Parallel capability**: Multiple subagents can run simultaneously for improved efficiency
- **Configuration dependent**: Available subagent types depend on your system configuration

## Configuration

Subagents are configured through Qwen Code's agent configuration system. Use the `/agents` command to:

- View available subagents
- Create new subagent configurations
- Modify existing subagent settings
- Set tool permissions and capabilities

For more information on configuring subagents, refer to the subagents documentation.

---

### Developers Tools Todo Write (`docs/developers/tools/todo-write.md`)

# Todo Write Tool (`todo_write`)

This document describes the `todo_write` tool for Qwen Code.

## Description

Use `todo_write` to create and manage a structured task list for your current coding session. This tool helps the AI assistant track progress and organize complex tasks, providing you with visibility into what work is being performed.

The tool is disabled by default. Enable it in `settings.json` and restart Qwen
Code:

```json
{
  "tools": {
    "todoWrite": {
      "enabled": true
    }
  }
}
```

### Arguments

`todo_write` takes one argument:

- `todos` (array, required): An array of todo items, where each item contains:
  - `content` (string, required): The description of the task.
  - `status` (string, required): The current status (`pending`, `in_progress`, or `completed`).
  - `id` (string, required): A unique identifier for the todo item.

## How to use `todo_write` with Qwen Code

When the tool is enabled, the AI assistant may use it for complex, multi-step tasks. You can also ask the assistant to create a todo list if you want to see the planned approach for your request.

The tool stores todo lists in your home directory (`~/.qwen/todos/`) with session-specific files, so each coding session maintains its own task list.

## When the AI uses this tool

The assistant uses `todo_write` for:

- Complex tasks requiring multiple steps
- Feature implementations with several components
- Refactoring operations across multiple files
- Any work involving 3 or more distinct actions

The assistant will not use this tool for simple, single-step tasks or purely informational requests.

### `todo_write` examples

Creating a feature implementation plan:

```
todo_write(todos=[
  {
    "id": "1",
    "content": "Create user preferences model",
    "status": "pending"
  },
  {
    "id": "2",
    "content": "Add API endpoints for preferences",
    "status": "pending"
  },
  {
    "id": "3",
    "content": "Implement frontend components",
    "status": "pending"
  }
])
```

## Important notes

- **Opt-in:** Set `tools.todoWrite.enabled` to `true` and restart Qwen Code before using the tool.
- **Automatic usage when enabled:** The AI assistant manages todo lists during complex tasks.
- **Progress visibility:** You'll see todo lists updated in real-time as work progresses.
- **Session isolation:** Each coding session has its own todo list that doesn't interfere with others.

---

### Developers Tools Web Fetch (`docs/developers/tools/web-fetch.md`)

# Web Fetch Tool (`web_fetch`)

This document describes the `web_fetch` tool for Qwen Code.

## Description

Use `web_fetch` to fetch content from a specified URL and process it using an AI model. The tool takes a URL and a prompt as input, fetches the URL content, and processes the content with the prompt using a small, fast model.

### Arguments

`web_fetch` takes three arguments:

- `url` (string, required): The URL to fetch content from. Must be a fully-formed valid URL starting with `http://` or `https://`.
- `prompt` (string, required): The prompt describing what information you want to extract from the page content.
- `format` (string, optional): Controls only the `Accept` header sent to the server, indicating your content preference. **All fetched content is normalized to plain text for LLM processing**, regardless of the format specified. Defaults to `"auto"` if not specified.
  - `"auto"` (default): Prefers markdown via content negotiation (`Accept: text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.1`), then falls back to HTML, plain text, or other content types. **Recommended for most use cases** as it can reduce token usage by up to 80% for servers that support markdown while still working with JSON-only APIs.
  - `"markdown"`: Prefers `Accept: text/markdown, */*;q=0.1`. Use when you explicitly need markdown content.
  - `"html"`: Prefers `Accept: text/html, */*;q=0.1`. Use when the server requires HTML in the Accept header. Content is still converted to plain text for LLM processing.
  - `"text"`: Prefers `Accept: text/plain, */*;q=0.1`. Use when you specifically need plain text content.

## How to use `web_fetch` with Qwen Code

To use `web_fetch` with Qwen Code, provide a URL and a prompt describing what you want to extract from that URL. The tool will ask for confirmation before fetching the URL. Once confirmed, the tool will fetch the content directly and process it using an AI model.

The tool automatically:

- Converts HTML to text when necessary
- Handles GitHub blob URLs (converting them to raw URLs)
- Upgrades HTTP URLs to HTTPS for security
- Supports content negotiation for markdown (reduces token usage significantly)

Usage:

```
web_fetch(url="https://example.com", prompt="Summarize the main points of this article")
```

With format specification:

```
web_fetch(url="https://example.com", prompt="Get the raw content", format="markdown")
```

## `web_fetch` examples

Summarize a single article:

```
web_fetch(url="https://example.com/news/latest", prompt="Can you summarize the main points of this article?")
```

Extract specific information:

```
web_fetch(url="https://arxiv.org/abs/2401.0001", prompt="What are the key findings and methodology described in this paper?")
```

Analyze GitHub documentation:

```
web_fetch(url="https://github.com/QwenLM/Qwen/blob/main/README.md", prompt="What are the installation steps and main features?")
```

Get markdown content (for servers supporting Markdown for Agents):

```
web_fetch(url="https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/", prompt="Extract the key information", format="markdown")
```

## Important notes

- **Single URL processing:** `web_fetch` processes one URL at a time. To analyze multiple URLs, make separate calls to the tool.
- **URL format:** The tool automatically upgrades HTTP URLs to HTTPS and converts GitHub blob URLs to raw format for better content access.
- **Content negotiation:** The tool supports "Markdown for Agents" content negotiation. When using `format="auto"` (default), it sends `Accept: text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.1`, allowing servers that support markdown to return it directly instead of HTML. The low-priority `*/*` fallback keeps JSON-only APIs and other non-text endpoints fetchable. This can reduce token usage by up to 80%.
- **Content processing:** The tool fetches content directly and processes it using an AI model. When the server returns HTML, it converts it to readable text format. When the server returns markdown, plain text, or another fallback content type such as JSON, it uses the content as-is.
- **Output quality:** The quality of the output will depend on the clarity of the instructions in the prompt.
- **MCP tools:** If an MCP-provided web fetch tool is available (starting with "mcp\_\_"), prefer using that tool as it may have fewer restrictions.

## Markdown for Agents Support

Qwen Code's `web_fetch` tool implements support for [Cloudflare's Markdown for Agents](https://blog.cloudflare.com/markdown-for-agents/) specification. This feature allows websites to serve markdown content directly to AI agents, significantly reducing token usage compared to parsing HTML.

### How it works

1. The `format` parameter controls **only** the `Accept` header sent to the server (it does not affect the output format):
   - `format="auto"`: sends `Accept: text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.1`
   - `format="markdown"`: sends `Accept: text/markdown, */*;q=0.1`
   - `format="html"`: sends `Accept: text/html, */*;q=0.1`
   - `format="text"`: sends `Accept: text/plain, */*;q=0.1`
2. If the server supports markdown, it returns `Content-Type: text/markdown`
3. The tool uses markdown or plain text content directly without conversion
4. If the server returns HTML, it converts to readable text format for LLM processing; markdown, plain text, and fallback content types such as JSON are used as-is
5. All content is normalized to text before being processed by the AI model

### Benefits

- **Token efficiency:** Markdown content typically uses 80% fewer tokens than equivalent HTML
- **Better structure:** Markdown preserves semantic structure (headings, lists, etc.)
- **Backward compatible:** Works with all websites, enhanced experience for supporting servers

### Example servers supporting markdown

- Cloudflare Developer Documentation
- Cloudflare Blog
- Any website using Cloudflare's "Markdown for Agents" feature

---

### Developers Tools Web Search (`docs/developers/tools/web-search.md`)

# Web Search

Qwen Code provides web search two ways:

1. **Built-in `web_search` tool** — backed by the DashScope Responses API server-side search. On by default at startup for supported ModelStudio and OpenAI-compatible DashScope configurations; no extra provider or MCP setup.
2. **MCP (Model Context Protocol) integrations** — connect any external search service (Tavily, GLM, and others). Use this when your provider cannot back the built-in tool.

## Built-in `web_search`

The built-in tool issues a self-contained search request to a small auxiliary model with DashScope's server-side `web_search` (and `web_extractor`) tools, and returns the narrated findings plus source URLs.

### When it turns on by itself

If you configured nothing under `tools.webSearch`, the tool registers whenever the model you are running can back the search request with the same credentials:

| How you signed in                                                                                                          | Built-in search                                 |
| -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| Alibaba ModelStudio → **Standard API Key**                                                                                 | on                                              |
| Alibaba ModelStudio → **Token Plan**                                                                                       | on                                              |
| Alibaba ModelStudio → **Coding Plan**                                                                                      | off — its endpoint is not verified for this API |
| QwenCloud → **Standard API Key**                                                                                           | on                                              |
| QwenCloud → **Token Plan**                                                                                                 | on                                              |
| An OpenAI-compatible `modelProviders` or Custom Provider entry on a recognized DashScope Responses host, with a direct key | on                                              |
| Third-party providers (OpenRouter, DeepSeek, ModelScope, …), custom endpoints on other hosts, local models                 | off                                             |

Searches bill the same key as your main model. Permission handling follows the active approval mode and rules; in `default` approval mode the first search asks for confirmation. When your provider cannot back the tool, it simply does not appear at startup — no startup warning.

To turn it off:

```json
{ "tools": { "webSearch": { "enabled": false } } }
```

or `ENABLE_WEB_SEARCH=false`. Bare mode and safe mode always disable it.

### Configuring it explicitly

Point the tool at a ModelStudio Standard/Token Plan or another verified DashScope Responses entry. This is useful when your main model runs on another provider and you also hold a separate supported DashScope key. Coding Plan hosts are excluded from automatic activation because the Responses search tools are not verified there. You can opt in explicitly with `tools.webSearch.model`; if the endpoint does not serve them, the first search fails loudly. Use an MCP search provider if you do not want to rely on that unverified path.

```json
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.8-flash",
        "envKey": "DASHSCOPE_API_KEY",
        "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1"
      }
    ]
  },
  "tools": {
    "webSearch": {
      "enabled": true,
      "model": "qwen3.8-flash"
    }
  }
}
```

| Setting                         | Env override                 | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tools.webSearch.enabled`       | `ENABLE_WEB_SEARCH`          | Set `false` to turn the tool off. Implicit startup activation requires leaving `enabled`, `model`, and the env-only backend unset. Setting `true` permits automatic derivation only when the env-only backend is also unset; otherwise a `model` is required.                                                                                                                                                                                                                                                                                                                          |
| `tools.webSearch.model`         | `WEB_SEARCH_MODEL`           | Search model selector for the explicit path (`modelId` or `authType:modelId`). With `WEB_SEARCH_BASE_URL` it is the plain model id for that endpoint; otherwise it must match a declared DashScope-compatible `modelProviders` entry. The automatic path uses `qwen3.8-flash`.                                                                                                                                                                                                                                                                                                         |
| `tools.webSearch.webExtractor`  | `WEB_SEARCH_EXTRACTOR`       | Let the search agent open result pages for better-grounded answers (default `true`; billed separately by DashScope).                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `tools.webSearch.timeoutMs`     | `WEB_SEARCH_TIMEOUT_MS`      | Total time budget for one search, in milliseconds (default `120000`, max `600000`; other values fall back to the default). A search that runs out of time returns what arrived as a partial result once at least one search call has completed; if the budget expires before the first search call finishes, the tool reports a timeout error instead, because narration with no executed search is not auditable evidence. A per-tool execution cap (`QWEN_CODE_TOOL_EXECUTION_TIMEOUT_MS`) below this budget fires first and discards the partial result; keep it above `timeoutMs`. |
| `tools.webSearch.maxPerSession` | `WEB_SEARCH_MAX_PER_SESSION` | Maximum `web_search` calls in one session (default `200`, max `10000`; other values fall back to the default). The count is shared with subagents and resets when the session changes (`/clear`, `/resume`, branching). Once it is reached, further searches are skipped and the model is told to continue with what it has gathered.                                                                                                                                                                                                                                                  |

### Env-only configuration (no settings.json)

For environments where you cannot write a settings file (locked-down containers, CI
with env injection only), the tool can be configured entirely through environment
variables — no `modelProviders` entry needed:

```bash
export ENABLE_WEB_SEARCH=true
export WEB_SEARCH_MODEL=qwen3.8-flash
export WEB_SEARCH_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
export DASHSCOPE_API_KEY=sk-...        # or set WEB_SEARCH_API_KEY instead
```

`WEB_SEARCH_BASE_URL` mirrors a `modelProviders` entry's `baseUrl` and must be a
DashScope-compatible endpoint; when it is set, it takes precedence over
`modelProviders` resolution and `WEB_SEARCH_MODEL` is used as the plain DashScope
model id. The API key is read from `WEB_SEARCH_API_KEY` if set, otherwise from
`DASHSCOPE_API_KEY`. Misconfiguration still surfaces as a startup notice.

Notes:

- The selector must resolve to a DashScope-compatible `modelProviders` entry carrying a direct API key via `envKey`. Your main model can be any provider — only the search side request needs a DashScope entry. Qwen OAuth cannot back the tool.
- Which providers can activate the tool is decided at startup. Once active, the search backend follows the currently selected model on the next search; switching to an unsupported provider makes that invocation fail, while switching from a session where the tool was absent still requires a restart to register it.
- Automatic host detection intentionally accepts only known DashScope regional hosts, Model Studio and QwenCloud MaaS hosts (including their `token-plan.` subscription subdomains), and internal Alibaba hosts. Generic `*.alicloudapi.com` gateways and `DASHSCOPE_PROXY_BASE_URL` are excluded because they are not known to forward the Responses search tools.
- If enabled explicitly but misconfigured, the tool stays off and a startup notice explains which condition failed. Automatic activation never emits a notice.
- Searches bill your DashScope key (`usage.x_tools` counts). Auto approval mode (the default) lets the classifier approve searches without prompting; in `default` approval mode the tool asks, and approving with "always allow" persists a standard `WebSearch` permission rule, like other tools.
- There is no client-side model allowlist; a model the Responses endpoint does not serve fails loudly on first use.
- A search that exceeds its time budget returns what arrived as a partial result once at least one search call has completed; if the budget expires before the first search call finishes, the tool reports a timeout error instead, because narration with no executed search is not auditable evidence. When a search call did complete but the narrated answer never arrived, the result carries at most 6,000 characters of the page text the agent had read, labeled as raw page content.
- The per-session cap counts `web_search` tool calls, not the searches one call runs internally, and a call that fails still counts because the request was sent. A skipped call is not an error: it tells the model the budget is used and to ask you to raise `tools.webSearch.maxPerSession` if more searches are genuinely needed.

## MCP alternatives

If your provider cannot back the built-in tool, web search is available by connecting an external MCP server — see the services below.

## ⚠️ Historical Breaking Change: original built-in `web_search` removed

> **Affected versions:** `V0.0.7+` through the last release with the original multi-provider built-in web search.

The original built-in `web_search` tool (Tavily/Google/GLM/DashScope multi-provider) and its configuration were **removed**. The built-in tool documented above is a different implementation with different configuration. If you were using any of the following, migrate either to the new built-in tool (DashScope) or to MCP:

| Removed                                                                | What to do                                                      |
| ---------------------------------------------------------------------- | --------------------------------------------------------------- |
| `webSearch` block in `settings.json`                                   | Configure an MCP server in `mcpServers` instead (see below)     |
| `advanced.tavilyApiKey` in `settings.json`                             | Use the [Tavily MCP server](#tavily-websearch)                  |
| `TAVILY_API_KEY` environment variable                                  | Use the [Tavily MCP server](#tavily-websearch)                  |
| `DASHSCOPE_API_KEY` for web search                                     | Use the [built-in `web_search` tool](#built-in-web_search)      |
| `GLM_API_KEY` for web search                                           | Use the [GLM WebSearch Prime MCP](#glm-websearch-prime-zhipuai) |
| `--tavily-api-key` / `--glm-api-key` / `--dashscope-api-key` CLI flags | Configure via `mcpServers` in `settings.json`                   |

### Migration Examples

**Before (Tavily via built-in tool):**

```json
{
  "webSearch": {
    "provider": [{ "type": "tavily", "apiKey": "tvly-xxx" }],
    "default": "tavily"
  }
}
```

**After (Tavily via MCP):**

```json
{
  "mcpServers": {
    "tavily": {
      "httpUrl": "https://mcp.tavily.com/mcp/?tavilyApiKey=tvly-xxx"
    }
  }
}
```

---

**Before (DashScope via built-in tool):**

```json
{
  "webSearch": {
    "provider": [{ "type": "dashscope", "apiKey": "sk-xxx" }],
    "default": "dashscope"
  }
}
```

**After (Alibaba Cloud Bailian WebSearch via MCP):**

```json
{
  "mcpServers": {
    "WebSearch": {
      "httpUrl": "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp",
      "headers": {
        "Authorization": "Bearer sk-xxx"
      }
    }
  }
}
```

---

## Supported MCP Web Search Services

### Alibaba Cloud Bailian WebSearch

The official web search MCP service provided by Alibaba Cloud Bailian platform, powered by DashScope. If you have a DashScope key, prefer the built-in `web_search` tool above — it uses a stronger search path than this MCP service.

- **MCP Marketplace:** https://bailian.console.aliyun.com/cn-beijing?tab=mcp#/mcp-market/detail/WebSearch
- **Cost:** Paid (billed via Alibaba Cloud DashScope)
- **Get API Key:** https://help.aliyun.com/zh/model-studio/get-api-key
- **Best for:** Chinese-language queries, access to Chinese web content, integration with the Alibaba Cloud ecosystem

#### Setup

**Method 1: CLI command**

```bash
qwen mcp add WebSearch \
  -t http \
  "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp" \
  -H "Authorization: Bearer ${DASHSCOPE_API_KEY}"
```

**Method 2: `settings.json`**

```json
{
  "mcpServers": {
    "WebSearch": {
      "httpUrl": "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp",
      "headers": {
        "Authorization": "Bearer ${DASHSCOPE_API_KEY}"
      }
    }
  }
}
```

Replace `${DASHSCOPE_API_KEY}` with your actual API key, or set it as an environment variable so Qwen Code picks it up automatically.

---

### Tavily WebSearch

A production-ready MCP server providing real-time web search, extract, map, and crawl capabilities.

- **Repository:** https://github.com/tavily-ai/tavily-mcp
- **Cost:** Paid (free tier available)
- **Get API Key:** https://app.tavily.com/home
- **Best for:** General-purpose web search with high-quality AI-generated answers

#### Available Tools

- `tavily_search` — Real-time web search
- `tavily_extract` — Intelligent data extraction from web pages
- `tavily_map` — Create a structured map of a website
- `tavily_crawl` — Systematically explore websites

#### Setup

**Method 1: CLI command (Remote MCP)**

```bash
qwen mcp add tavily \
  -t http \
  "https://mcp.tavily.com/mcp/?tavilyApiKey=${TAVILY_API_KEY}"
```

**Method 2: `settings.json` (Remote MCP)**

```json
{
  "mcpServers": {
    "tavily": {
      "httpUrl": "https://mcp.tavily.com/mcp/?tavilyApiKey=${TAVILY_API_KEY}"
    }
  }
}
```

Replace `${TAVILY_API_KEY}` with your actual API key, or set it as an environment variable.

**Method 3: `settings.json` (Local NPX)**

```json
{
  "mcpServers": {
    "tavily-mcp": {
      "command": "npx",
      "args": ["-y", "tavily-mcp@latest"],
      "env": {
        "TAVILY_API_KEY": "your-api-key-here"
      }
    }
  }
}
```

---

### GLM WebSearch Prime (ZhipuAI)

The official web search Remote MCP service provided by ZhipuAI (智谱AI), designed for GLM Coding Plan users. Provides real-time web search including news, stock prices, weather, and more.

- **Documentation:** https://docs.bigmodel.cn/cn/coding-plan/mcp/search-mcp-server
- **Cost:** Included in GLM Coding Plan subscription (Lite: 100 calls/month, Pro: 1,000/month, Max: 4,000/month)
- **Get API Key:** https://open.bigmodel.cn/apikey/platform
- **Best for:** Chinese-language queries, real-time information retrieval

#### Available Tools

- `webSearchPrime` — Web search returning page title, URL, summary, site name, and favicon

#### Setup

**Method 1: CLI command**

```bash
qwen mcp add web-search-prime \
  -t http \
  "https://open.bigmodel.cn/api/mcp/web_search_prime/mcp" \
  -H "Authorization: Bearer ${GLM_API_KEY}"
```

**Method 2: `settings.json`**

```json
{
  "mcpServers": {
    "web-search-prime": {
      "httpUrl": "https://open.bigmodel.cn/api/mcp/web_search_prime/mcp",
      "headers": {
        "Authorization": "Bearer ${GLM_API_KEY}"
      }
    }
  }
}
```

Replace `${GLM_API_KEY}` with your actual ZhipuAI API key, or set it as an environment variable.

---

### Firecrawl

The official hosted MCP server from Firecrawl, providing web search, page scraping, website crawling and mapping, browser interaction, and file parsing. Search results can include each page's content as Markdown, so the model can read its sources without a separate fetch step. The hosted server works without an API key for search, scrape, and parse. Search queries, the URLs you ask it to read, and any files you parse are sent to Firecrawl.

- **Documentation:** https://docs.firecrawl.dev/mcp-server
- **Cost:** No API key needed for search, scrape, and parse (daily limit per IP); paid plans for higher limits
- **Get API Key:** https://www.firecrawl.dev/app/api-keys
- **Best for:** Web search where the model also needs to read the pages it finds, and trying MCP web search without an API key

#### Available Tools

- `firecrawl_search` — Web search, optionally returning each result's page content as Markdown
- `firecrawl_scrape` — Get a single page as clean Markdown
- `firecrawl_parse` — Convert a local file (PDF, Word, spreadsheet, and more) to Markdown

With an API key, the server also offers tools to map and crawl websites, interact with pages (click, type, navigate), and search developer sources like GitHub issues and docs.

#### Setup

**Method 1: CLI command (Remote MCP)**

```bash
qwen mcp add firecrawl \
  -t http \
  "https://mcp.firecrawl.dev/v2/mcp" \
  -H "Authorization: Bearer ${FIRECRAWL_API_KEY}"
```

To try it without a key, leave out the `-H` line. You get `firecrawl_search`, `firecrawl_scrape`, and `firecrawl_parse` within a daily per-IP limit.

**Method 2: `settings.json` (Remote MCP)**

```json
{
  "mcpServers": {
    "firecrawl": {
      "httpUrl": "https://mcp.firecrawl.dev/v2/mcp",
      "headers": {
        "Authorization": "Bearer ${FIRECRAWL_API_KEY}"
      }
    }
  }
}
```

Replace `${FIRECRAWL_API_KEY}` with your actual API key, or set it as an environment variable. If the key is empty, the server still connects but only offers the three keyless tools.

**Method 3: `settings.json` (Local NPX)**

```json
{
  "mcpServers": {
    "firecrawl-mcp": {
      "command": "npx",
      "args": ["-y", "firecrawl-mcp@latest"],
      "env": {
        "FIRECRAWL_API_KEY": "your-api-key-here"
      }
    }
  }
}
```

---

### SerpApi

The official hosted MCP server by SerpApi, providing real-time structured search results from Google, Baidu, Bing and other search engines through a single tool. Search queries and their parameters are sent to SerpApi.

- **Documentation:** https://serpapi.com/integrations/mcp
- **Cost:** Paid (free plan available)
- **Get API Key:** https://serpapi.com/manage-api-key
- **Best for:** Structured results from a broad choice of search engines

#### Available Tools

- `search` — Web search, optionally returning results as Markdown

#### Setup

**Method 1: CLI command**

```bash
qwen mcp add serpapi \
  -t http \
  "https://mcp.serpapi.com/mcp" \
  -H "Authorization: Bearer ${SERPAPI_API_KEY}"
```

**Method 2: `settings.json`**

```json
{
  "mcpServers": {
    "serpapi": {
      "httpUrl": "https://mcp.serpapi.com/mcp",
      "headers": {
        "Authorization": "Bearer ${SERPAPI_API_KEY}"
      }
    }
  }
}
```

Replace `${SERPAPI_API_KEY}` with your actual API key, or set it as an environment variable. The key is only needed for searches. Without a valid API key, the server still connects and lists its tools, but `search` returns an error.

---

### looot

A hosted MCP server from looot with web search, Google SERP results, news search and web page reading. Search queries and the URLs you ask it to read are sent to looot.

- **Documentation:** https://docs.looot.ai
- **Cost:** Paid (prepaid balance, no free tier)
- **Get API Key:** https://looot.ai
- **Best for:** Web search, Google SERP and news results, and reading a page as text

#### Available Tools

- `web_search` — Web search
- `google_search_results` — Google SERP results
- `news_search` — News search
- `read_web_page` — Read a single page as text

The server also exposes company and email lookups.

#### Setup

**Method 1: CLI command (Remote MCP)**

```bash
qwen mcp add looot \
  -t http \
  "https://api.looot.ai/mcp" \
  -H "Authorization: Bearer ${LOOOT_API_KEY}"
```

**Method 2: `settings.json` (Remote MCP)**

```json
{
  "mcpServers": {
    "looot": {
      "httpUrl": "https://api.looot.ai/mcp",
      "headers": {
        "Authorization": "Bearer ${LOOOT_API_KEY}"
      }
    }
  }
}
```

Replace `${LOOOT_API_KEY}` with your actual API key, or set it as an environment variable.

---

---

### Users Configuration Auth (`docs/users/configuration/auth.md`)

# Authentication

Qwen Code's first-run `/auth` menu has four top-level options. Pick the one that matches how you want to run the CLI:

- **QwenCloud**: official recommended setup. Opens a sub-menu with **Token Plan** (subscription with a monthly Credit quota · dedicated endpoint, Personal and Team editions) or **Standard API Key** (pay-as-you-go). You then pick the platform explicitly — **China — 千问AI平台** or **International — QwenCloud** — so the UI language never decides your endpoint. Both access methods also enable the built-in [`web_search` tool](../../developers/tools/web-search.md) with no extra setup.
- **Alibaba ModelStudio**: opens a sub-menu with **Coding Plan** (for individual developers · weekly quota included), **Token Plan** (for teams and companies · usage-based billing with a dedicated endpoint), or **Standard API Key** (connect with an existing ModelStudio API key). Token Plan and Standard API Key also enable the built-in [`web_search` tool](../../developers/tools/web-search.md) with no extra setup. Saved ModelStudio credentials, endpoints and models keep working unchanged.
- **Third-party Providers**: choose a built-in provider and connect with an API key (DeepSeek, Grok, MiniMax, Z.AI, Kimi, Idealab, ModelScope, OpenRouter, Requesty).
- **Custom Provider**: manually connect a local server, proxy, or unsupported provider — supports OpenAI, Anthropic, Gemini, and other compatible endpoints.

> [!note]
>
> **Qwen OAuth** is no longer a selectable dialog entry — its free tier was discontinued on 2026-04-15. It remains documented below as a hard-coded, discontinued provider only.

## ☁️ Option 1: QwenCloud

The official platform entry. Choose it when your API key comes from 千问AI平台 (China) or QwenCloud (International).

- **How it works**: pick an access method, then pick the platform explicitly — the UI language never decides your endpoint. Enter the key issued by that platform and Qwen Code configures the matching endpoint.
- **Access methods**: **Token Plan** (a subscription with a monthly Credit quota on a dedicated endpoint; Personal and Team editions) or **Standard API Key** (pay-as-you-go). Coding Plan is _not_ offered here — it is an Alibaba ModelStudio product, see Option 3.
- **Requirements**: a Token Plan key (prefixed `sk-sp-`) generated from the Token Plan console, or a pay-as-you-go key (prefixed `sk-` or `sk-ws-`) from the platform's API key page.
- **Benefits**: both access methods enable the built-in [`web_search` tool](../../developers/tools/web-search.md) with no extra setup.

| Platform                  | Access method    | Endpoint                                                      | API key                                                                                  |
| ------------------------- | ---------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| China — 千问AI平台        | Standard API Key | `https://maas.qianwenaiapi.com/compatible-mode/v1`            | [API keys](https://platform.qianwenai.com/docs/developer-guides/administration/api-keys) |
| China — 千问AI平台        | Token Plan       | `https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1` | [Token Plan](https://platform.qianwenai.com/docs/token-plan/overview)                    |
| International — QwenCloud | Standard API Key | `https://maas.qwencloudapi.com/compatible-mode/v1`            | [API keys](https://docs.qwencloud.com/developer-guides/administration/api-keys)          |
| International — QwenCloud | Token Plan       | `https://token-plan.maas.qwencloudapi.com/compatible-mode/v1` | [Token Plan](https://docs.qwencloud.com/token-plan/overview)                             |

### Interactive setup

Enter `qwen` in the terminal to launch Qwen Code, then run the `/auth` command, select **QwenCloud**, and choose **Token Plan** or **Standard API Key** from the sub-menu. Choose the platform (**China — 千问AI平台** or **International — QwenCloud**), then enter your API key.

### Headless or scripted setup

For CI, containers, or scripts, configure the platform with environment variables instead of the interactive `/auth` flow.

```bash
# Pay-as-you-go (Standard API Key), International
export QWENCLOUD_API_KEY="sk-your-api-key"
export OPENAI_BASE_URL="https://maas.qwencloudapi.com/compatible-mode/v1"
export OPENAI_MODEL="qwen3.8-max"
```

```bash
# Token Plan, China
export QWENCLOUD_TOKEN_PLAN_API_KEY="sk-sp-your-api-key"
export OPENAI_BASE_URL="https://token-plan.maas.qianwenaiapi.com/compatible-mode/v1"
export OPENAI_MODEL="qwen3.8-max"
```

### Alternative: configure via `settings.json`

If you prefer to skip the interactive `/auth` flow, add the following to `~/.qwen/settings.json`:

```json
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.8-max",
        "name": "qwen3.8-max (QwenCloud)",
        "baseUrl": "https://maas.qwencloudapi.com/compatible-mode/v1",
        "description": "qwen3.8-max from QwenCloud Standard API Key",
        "envKey": "QWENCLOUD_API_KEY"
      }
    ]
  },
  "env": {
    "QWENCLOUD_API_KEY": "sk-your-api-key"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3.8-max"
  }
}
```

> [!note]
>
> `capabilities` is the one provider stamp that is never inferred from the model name, so a hand-written entry resolves with no reasoning declaration and `/effort` offers the generic tier list rather than QwenCloud's curated `low` / `medium` / `xhigh`. Use `/auth` to install the provider when you want the curated set.

As with the other provider-specific keys, `QWENCLOUD_API_KEY` and `QWENCLOUD_TOKEN_PLAN_API_KEY` take effect once a `settings.json` provider entry declares them as the `envKey`; to select OpenAI-compatible auth with environment variables alone, export `OPENAI_API_KEY` instead.

> [!note]
>
> QwenCloud and Alibaba ModelStudio are separate entries with separate env keys and endpoints. Choosing QwenCloud does not rewrite or delete saved ModelStudio credentials, endpoints, or model selections.

## Option 2: Qwen OAuth (Discontinued)

> [!warning]
>
> The Qwen OAuth free tier was discontinued on 2026-04-15. Existing cached tokens may continue working briefly, but new requests will be rejected. Please switch to Alibaba Cloud Coding Plan, [OpenRouter](https://openrouter.ai), [Fireworks AI](https://app.fireworks.ai), or another provider. Run `qwen` and use `/auth` to configure.

- **How it works**: on first start, Qwen Code opens a browser login page. After you finish, credentials are cached locally so you usually won't need to log in again.
- **Requirements**: a `qwen.ai` account + internet access (at least for the first login).
- **Benefits**: no API key management, automatic credential refresh.
- **Cost & quota**: the free tier has been discontinued as of 2026-04-15.

Start the CLI and follow the browser flow:

```bash
qwen
```

Qwen OAuth is no longer offered as a selectable entry in the `/auth` dialog; run `/auth` and choose one of the current options (QwenCloud, Alibaba ModelStudio, Third-party Providers, or Custom Provider) instead.

> [!note]
>
> In non-interactive or headless environments (e.g., CI, SSH, containers), you typically **cannot** complete the OAuth browser login flow.
> In these cases, please use the Alibaba Cloud Coding Plan or API Key authentication method.

## 💳 Option 3: Alibaba Cloud Coding Plan

Use this if you want predictable costs with diverse model options and higher usage quotas.

- **How it works**: Subscribe to the Coding Plan with a fixed monthly fee, then configure Qwen Code to use the dedicated endpoint and your subscription API key.
- **Requirements**: Obtain an active Coding Plan subscription from [Alibaba Cloud ModelStudio(Beijing)](https://bailian.console.aliyun.com/cn-beijing?tab=coding-plan#/efm/coding-plan-index) or [Alibaba Cloud ModelStudio(intl)](https://modelstudio.console.alibabacloud.com/?tab=coding-plan#/efm/coding-plan-index), depending on the region of your account.
- **Benefits**: Diverse model options, higher usage quotas, predictable monthly costs, access to a wide range of models (Qwen, GLM, Kimi, Minimax and more).
- **Cost & quota**: View Aliyun ModelStudio Coding Plan documentation[Beijing](https://bailian.console.aliyun.com/cn-beijing/?tab=doc#/doc/?type=model&url=3005961)[intl](https://modelstudio.console.alibabacloud.com/?tab=doc#/doc/?type=model&url=2840914).

Alibaba Cloud Coding Plan is available in two regions:

| Region                       | Console URL                                                                  |
| ---------------------------- | ---------------------------------------------------------------------------- |
| Aliyun ModelStudio (Beijing) | [bailian.console.aliyun.com](https://bailian.console.aliyun.com)             |
| Alibaba Cloud (intl)         | [bailian.console.alibabacloud.com](https://bailian.console.alibabacloud.com) |

### Interactive setup

Enter `qwen` in the terminal to launch Qwen Code, then run the `/auth` command, select **Alibaba ModelStudio**, and choose **Coding Plan** from the sub-menu. Choose your region, then enter your `sk-sp-xxxxxxxxx` key.

After authentication, use the `/model` command to switch between all Alibaba Cloud Coding Plan supported models (including qwen3.5-plus, qwen3.6-plus, qwen3.7-plus, qwen3-coder-plus, qwen3-coder-next, qwen3-max-2026-01-23, glm-5, glm-4.7, kimi-k2.5, and MiniMax-M2.5).

### Headless or scripted setup

For CI, containers, or scripts, configure Coding Plan with environment variables or `settings.json` instead of the removed `qwen auth coding-plan` command.

```bash
export BAILIAN_CODING_PLAN_API_KEY="sk-sp-xxxxxxxxx"
export OPENAI_BASE_URL="https://coding.dashscope.aliyuncs.com/v1"
export OPENAI_MODEL="qwen3-coder-plus"
```

Use `https://coding.dashscope.aliyuncs.com/v1` for the China (Beijing) endpoint, or `https://coding-intl.dashscope.aliyuncs.com/v1` for the international endpoint.

### Alternative: configure via `settings.json`

If you prefer to skip the interactive `/auth` flow, add the following to `~/.qwen/settings.json`:

```json
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3-coder-plus",
        "name": "qwen3-coder-plus (Coding Plan)",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
        "description": "qwen3-coder-plus from Alibaba Cloud Coding Plan",
        "envKey": "BAILIAN_CODING_PLAN_API_KEY"
      }
    ]
  },
  "env": {
    "BAILIAN_CODING_PLAN_API_KEY": "sk-sp-xxxxxxxxx"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3-coder-plus"
  }
}
```

> [!note]
>
> The Coding Plan uses a dedicated endpoint (`https://coding.dashscope.aliyuncs.com/v1`) that is different from the standard Dashscope endpoint. Make sure to use the correct `baseUrl`.

## 🪙 Option 4: Alibaba Cloud Token Plan

Use this if your team or company prefers usage-based billing on a dedicated ModelStudio endpoint.

- **How it works**: Subscribe to the Token Plan in Alibaba Cloud ModelStudio, then configure Qwen Code to use the region-specific Token Plan endpoint and your API key. You are billed for actual token usage instead of a fixed monthly fee.
- **Requirements**: Obtain a Token Plan API key from [Alibaba Cloud ModelStudio(Beijing)](https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856) or [Alibaba Cloud ModelStudio(intl)](https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model), depending on the region of your account.
- **Benefits**: Usage-based billing for teams and companies, dedicated region-specific endpoints, access to a wide range of models (Qwen, DeepSeek, GLM, Kimi, Minimax and more).
- **Cost & quota**: View Alibaba Cloud ModelStudio Token Plan documentation [Beijing](https://bailian.console.aliyun.com/cn-beijing?tab=doc#/doc/?type=model&url=3028856) [intl](https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model).

Token Plan is available in two regions, each with its own dedicated endpoint:

| Region                    | Endpoint                                                                 | Console URL                                                                                         |
| ------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| China (Beijing)           | `https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1`     | [bailian.console.aliyun.com](https://bailian.console.aliyun.com/cn-beijing)                         |
| Singapore (International) | `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` | [modelstudio.console.alibabacloud.com](https://modelstudio.console.alibabacloud.com/ap-southeast-1) |

### Interactive setup

Enter `qwen` in the terminal to launch Qwen Code, then run the `/auth` command, select **Alibaba ModelStudio**, and choose **Token Plan** from the sub-menu. Choose your region (**China (Beijing)** or **Singapore (International)**), then enter your API key. The wizard then shows its final step (Step 3/3 · Model IDs), where you pick the model IDs to configure: the models served by your endpoint are offered there and are applied only when you explicitly select them. ModelStudio Token Plan API keys have no prefix requirement, unlike Coding Plan keys, which start with `sk-sp-`, and unlike the separate QwenCloud Token Plan keys in Option 1, which also start with `sk-sp-`.

After authentication, use the `/model` command to browse and switch between the models configured for your Token Plan. The model lineup evolves over time, so it is intentionally not listed here; model discovery from the endpoint happens during the `/auth` setup step above (the endpoint's own list is offered there and must be selected explicitly), and `/model` then shows the models configured for your plan.

### Headless or scripted setup

For CI, containers, or scripts, configure Token Plan with environment variables or `settings.json` instead of the interactive `/auth` flow.

```bash
export BAILIAN_TOKEN_PLAN_API_KEY="your-api-key"
export OPENAI_BASE_URL="https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"
export OPENAI_MODEL="qwen3.7-plus"
```

Use `https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1` for the China (Beijing) endpoint, or `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` for the international (Singapore) endpoint. Replace `qwen3.7-plus` with any model included in your plan.

Note that `BAILIAN_TOKEN_PLAN_API_KEY` is a provider-specific key: it takes effect once the `settings.json` provider entry in the next section exists, because that entry declares it as the `envKey`. To select OpenAI-compatible auth with environment variables alone, export `OPENAI_API_KEY` instead — provider-specific keys do not select the auth type by themselves.

### Alternative: configure via `settings.json`

If you prefer to skip the interactive `/auth` flow, add the following to `~/.qwen/settings.json`:

```json
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.7-plus",
        "name": "qwen3.7-plus (Token Plan)",
        "baseUrl": "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
        "description": "qwen3.7-plus from Alibaba Cloud Token Plan",
        "envKey": "BAILIAN_TOKEN_PLAN_API_KEY"
      }
    ]
  },
  "env": {
    "BAILIAN_TOKEN_PLAN_API_KEY": "your-api-key"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3.7-plus"
  }
}
```

> [!note]
>
> The Token Plan uses dedicated, region-specific endpoints (`*.maas.aliyuncs.com`) that are different from the standard DashScope endpoint. Make sure the `baseUrl` matches the region of your subscription.

## 🚀 Option 5: API Key (flexible)

Use this if you want to connect to third-party providers such as OpenAI, Anthropic, Google, Azure OpenAI, OpenRouter, Requesty, ModelScope, or a self-hosted endpoint. Supports multiple protocols and providers.

### Recommended: One-file setup via `settings.json`

The simplest way to get started with API Key authentication is to put everything in a single `~/.qwen/settings.json` file. Here's a complete, ready-to-use example:

```json
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3-coder-plus",
        "name": "qwen3-coder-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-coder-plus"
  }
}
```

What each field does:

| Field                        | Description                                                                                                                                     |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `modelProviders`             | Declares which models are available and how to connect to them. Keys (`openai`, `anthropic`, `gemini`) represent the API protocol.              |
| `env`                        | Stores API keys directly in `settings.json` as a fallback (lowest priority — shell `export` and `.env` files take precedence).                  |
| `security.auth.selectedType` | Tells Qwen Code which protocol to use on startup (e.g. `openai`, `anthropic`, `gemini`). Without this, you'd need to run `/auth` interactively. |
| `model.name`                 | The default model to activate when Qwen Code starts. Must match one of the `id` values in your `modelProviders`.                                |

After saving the file, just run `qwen` — no interactive `/auth` setup needed.

> [!tip]
>
> The sections below explain each part in more detail. If the quick example above works for you, feel free to skip ahead to [Security notes](#security-notes).

The key concept is **Model Providers** (`modelProviders`): Qwen Code supports multiple API protocols, not just OpenAI. You configure which providers and models are available by editing `~/.qwen/settings.json`, then switch between them at runtime with the `/model` command.

#### Supported protocols

| Protocol          | `modelProviders` key | Environment variables                                                                                                                                          | Providers                                                                                             |
| ----------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| OpenAI-compatible | `openai`             | `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL` (alias: `QWEN_MODEL`)                                                                                      | OpenAI, Azure OpenAI, OpenRouter, Requesty, ModelScope, Alibaba Cloud, any OpenAI-compatible endpoint |
| Anthropic         | `anthropic`          | `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_MODEL`                                                                                                   | Anthropic Claude                                                                                      |
| Google GenAI      | `gemini`             | `GEMINI_API_KEY`, `GEMINI_MODEL`                                                                                                                               | Google Gemini                                                                                         |
| Vertex AI         | `vertex-ai`          | `GOOGLE_API_KEY` + `GOOGLE_MODEL` (sets `GOOGLE_GENAI_USE_VERTEXAI=true`) or `GOOGLE_CLOUD_PROJECT` + `GOOGLE_MODEL` (keyless ADC); uses the `gemini` protocol | Google Vertex AI                                                                                      |

#### Step 1: Configure models and providers in `~/.qwen/settings.json`

Define which models are available for each protocol. Each model entry requires at minimum an `id`; `envKey` (the environment variable name that holds your API key) is optional and recommended — when omitted, it falls back to the auth type's default env key (e.g. `OPENAI_API_KEY` for `openai`).

> [!important]
>
> It is recommended to define `modelProviders` in the user-scope `~/.qwen/settings.json` to avoid merge conflicts between project and user settings.

Edit `~/.qwen/settings.json` (create it if it doesn't exist). You can mix multiple protocols in a single file — here is a multi-provider example showing just the `modelProviders` section:

```json
{
  "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"
      }
    ]
  }
}
```

> [!tip]
>
> Don't forget to also set `env`, `security.auth.selectedType`, and `model.name` alongside `modelProviders` — see the [complete example above](#recommended-one-file-setup-via-settingsjson) for reference.

**`ModelConfig` fields (each entry inside `modelProviders`):**

| Field              | Required | Description                                                                                                                                        |
| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`               | Yes      | Model ID sent to the API (e.g. `gpt-4o`, `claude-sonnet-4-20250514`)                                                                               |
| `wireApi`          | No       | OpenAI-compatible request format: `chat-completions` or `responses`. Omit to inherit the provider protocol.                                        |
| `name`             | No       | Display name in the `/model` picker (defaults to `id`)                                                                                             |
| `envKey`           | No       | Environment variable name for the API key (e.g. `OPENAI_API_KEY`); optional/recommended — defaults to the auth type's default env key when omitted |
| `baseUrl`          | No       | API endpoint override (useful for proxies or custom endpoints)                                                                                     |
| `generationConfig` | No       | Fine-tune `timeout`, `maxRetries`, `samplingParams`, etc.                                                                                          |

> [!note]
>
> When using the `env` field in `settings.json`, credentials are stored in plain text. For better security, prefer `.env` files or shell `export` — see [Step 2](#step-2-set-environment-variables).

For the full `modelProviders` schema and advanced options like `generationConfig`, `customHeaders`, and `extra_body`, see [Model Providers Reference](model-providers.md).

#### Step 2: Set environment variables

Qwen Code reads API keys from environment variables (specified by `envKey` in your model config). There are multiple ways to provide them, listed below from **highest to lowest priority**:

**1. Shell environment / `export` (highest priority)**

Set directly in your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) or inline before launching:

```bash

# Alibaba Dashscope
export DASHSCOPE_API_KEY="sk-..."

# OpenAI / OpenAI-compatible
export OPENAI_API_KEY="sk-..."

# Anthropic
export ANTHROPIC_API_KEY="sk-ant-..."

# Google GenAI
export GEMINI_API_KEY="AIza..."
```

**2. `.env` files**

Qwen Code auto-loads the **first** `.env` file it finds (variables are **not merged** across multiple files). Only variables not already present in `process.env` are loaded.

Search order (from the current directory, walking upward toward `/`):

1. `.qwen/.env` (preferred — keeps Qwen Code variables isolated from other tools)
2. `.env`

If nothing is found, it falls back to your **home directory**:

3. `~/.qwen/.env`
4. `~/.env`

> [!tip]
>
> `.qwen/.env` is recommended over `.env` to avoid conflicts with other tools. Some variables (like `DEBUG` and `DEBUG_MODE`) are excluded from project-level `.env` files to avoid interfering with Qwen Code behavior.

**3. `settings.json` → `env` field (lowest priority)**

You can also define API keys directly in `~/.qwen/settings.json` under the `env` key. These are loaded as the **lowest-priority fallback** — only applied when a variable is not already set by the system environment or `.env` files.

```json
{
  "env": {
    "DASHSCOPE_API_KEY": "sk-...",
    "OPENAI_API_KEY": "sk-...",
    "ANTHROPIC_API_KEY": "sk-ant-..."
  }
}
```

This is the approach used in the [one-file setup example](#recommended-one-file-setup-via-settingsjson) above. It's convenient for keeping everything in one place, but be mindful that `settings.json` may be shared or synced — prefer `.env` files for sensitive secrets.

**Priority summary:**

| Priority    | Source                         | Override behavior                            |
| ----------- | ------------------------------ | -------------------------------------------- |
| 1 (highest) | CLI flags (`--openai-api-key`) | Always wins                                  |
| 2           | System env (`export`, inline)  | Overrides `.env` and `settings.json` → `env` |
| 3           | `.env` file                    | Only sets if not in system env               |
| 4 (lowest)  | `settings.json` → `env`        | Only sets if not in system env or `.env`     |

#### Step 3: Switch models with `/model`

After launching Qwen Code, use the `/model` command to switch between all configured models. Models are grouped by protocol:

```
/model
```

The picker will show all models from your `modelProviders` configuration, grouped by their protocol (e.g. `openai`, `anthropic`, `gemini`). Your selection is persisted across sessions.

You can also switch models directly with a command-line argument, which is convenient when working across multiple terminals.

```bash
# In one terminal

qwen --model "qwen3-coder-plus"

# In another terminal

qwen --model "qwen3.5-plus"
```

## Removed `qwen auth` CLI command

The standalone `qwen auth` CLI command has been removed. Use these replacements instead:

| Previous use case                | Replacement                                                                                   |
| -------------------------------- | --------------------------------------------------------------------------------------------- |
| Interactive authentication setup | Run `qwen`, then use `/auth`                                                                  |
| Coding Plan setup                | Use `/auth`, or set `BAILIAN_CODING_PLAN_API_KEY` with the Coding Plan base URL               |
| Token Plan setup                 | Use `/auth`, or set `BAILIAN_TOKEN_PLAN_API_KEY` with the Token Plan base URL for your region |
| OpenRouter setup                 | Use `/auth`, or set `OPENROUTER_API_KEY` and `OPENAI_BASE_URL=https://openrouter.ai/api/v1`   |
| Requesty setup                   | Use `/auth`, or set `REQUESTY_API_KEY` and `OPENAI_BASE_URL=https://router.requesty.ai/v1`    |
| API-key or custom provider setup | Configure `~/.qwen/settings.json`, `.env`, or provider-specific environment variables         |
| Check current authentication     | Run `/doctor` inside Qwen Code                                                                |
| OAuth browser flow               | Run `qwen` interactively and use `/auth`; OAuth cannot be configured with env vars alone      |

Legacy invocations such as `qwen auth status` print a removal notice that summarizes these migration paths, including a Token Plan entry with the base URLs for both regions.

## Security notes

- Don't commit API keys to version control.
- Prefer `.qwen/.env` for project-local secrets (and keep it out of git).
- Treat your terminal output as sensitive if it prints credentials for verification.

---

### Users Configuration Model Providers (`docs/users/configuration/model-providers.md`)

# Model Providers

Qwen Code allows you to configure multiple model providers through the `modelProviders` setting in your `settings.json`. This enables you to switch between different AI models and providers using the `/model` command.

## Overview

Use `modelProviders` to declare models per provider id that the `/model` picker can switch between. Each key is a provider id and its value is **an array of model definitions** (`ModelConfig[]`). For built-in providers the key must be a valid auth type (`openai`, `anthropic`, `gemini`, `vertex-ai`); a custom provider id (e.g. `idealab`) is allowed as long as you map it to a protocol via the top-level [`providerProtocol`](#custom-provider-ids-providerprotocol) setting. Each model entry requires an `id`; `envKey` is **optional and recommended** (when omitted, it falls back to the auth type's default env key, e.g. `OPENAI_API_KEY` for `openai`), with optional `name`, `description`, `baseUrl`, and `generationConfig`. Credentials are never persisted in settings; the runtime reads them from `process.env[envKey]`. Qwen OAuth models remain hard-coded and cannot be overridden.

> [!note]
>
> Earlier previews wrapped each provider's models in a `{ "protocol": ..., "models": [...] }` object. That shape has been reverted — the current value is the bare `ModelConfig[]` array shown throughout this page. A wrapped entry in an already-migrated (`$version: 4`) settings file is silently skipped, so update any old configs to the array form.

> [!note]
>
> Only the `/model` command exposes non-default auth types. Anthropic, Gemini, etc., must be defined via `modelProviders`. The `/auth` command lists four top-level options: **QwenCloud** (with Token Plan and Standard API Key in its sub-menu), **Alibaba ModelStudio** (with Coding Plan, Token Plan, and Standard API Key in its sub-menu), **Third-party Providers**, and **Custom Provider**. (Qwen OAuth is no longer a selectable dialog entry; its free tier was discontinued on 2026-04-15.)

> [!note]
>
> **Model uniqueness:** Models are identified by their effective API protocol, `id`, and configured `baseUrl`. You can define the same model and URL with both `wireApi: "chat-completions"` and `wireApi: "responses"`, or use different URLs for the same model and API. If entries share all three values, the first occurrence wins and subsequent duplicates are skipped with a warning.

> [!note]
>
> **Hot reload vs. restart:** `modelProviders` edits in `settings.json` are picked up by a running interactive session without a restart (the file watcher debounces ~300ms; reopen `/model` to see new entries, the current selection is kept). Changing the active model's `wireApi` creates a different route; select that route explicitly or restart to use it. Invalid API edits leave the prior registry usable. `providerProtocol` is read once at startup and **requires a restart**.

### Image generation routes

Set `supportsImageGeneration: true` when a route can be used by the built-in
`image_gen` tool. This capability is independent from image input support such
as `capabilities.vision` or `generationConfig.modalities.image`.

Use `imageOnly: true` when the route is dedicated to image generation and must
not appear in ordinary model selectors. For backward compatibility,
`imageOnly: true` also implies image-generation capability, so existing settings
do not need to be migrated.

A dual-role route can be selected both as the main model and through
`/model --image`:

```json
{
  "modelProviders": {
    "openai": [
      {
        "id": "omni-model",
        "envKey": "MODEL_API_KEY",
        "baseUrl": "https://gateway.example.com/model-api",
        "supportsImageGeneration": true
      }
    ]
  }
}
```

A dedicated image route sets both fields. The legacy form with only
`imageOnly: true` remains valid:

```json
{
  "id": "image-model",
  "envKey": "MODEL_API_KEY",
  "baseUrl": "https://images.example.com/api/v1",
  "supportsImageGeneration": true,
  "imageOnly": true
}
```

The selected route must declare an explicit HTTPS `baseUrl` and a non-empty
`envKey`. Image generation uses the same endpoint and credential as the route;
if chat and image generation require different endpoints or credentials,
configure two routes instead.

### Live Voice routes

Set `realtimeOnly: true` on a route that speaks the DashScope Realtime
(speech-to-speech) protocol. Such a route is never offered as a chat, fast,
vision, voice or image model; it can only be chosen as the Live Voice model
through `experimental.liveVoice.model` (a `modelId`, or `provider:modelId` when
the same id exists under more than one provider).

```json
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.5-omni-plus-realtime",
        "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "envKey": "DASHSCOPE_API_KEY",
        "realtimeOnly": true
      }
    ]
  },
  "experimental": {
    "liveVoice": {
      "enabled": true,
      "model": "qwen3.5-omni-plus-realtime",
      "voice": "Tina"
    }
  }
}
```

When the selected model names a `realtimeOnly` route, Live Voice reads the API
key through the route's `envKey` (process environment first, then the `env`
block of `settings.json`) and derives the WebSocket endpoint from its `baseUrl`
(`https://<host>/compatible-mode/v1` becomes `wss://<host>/api-ws/v1/realtime`).
The route must declare both `baseUrl` and `envKey`, and the host must be a
DashScope endpoint.

Two rules are deliberate:

- Workspace settings are never consulted, only user- and system-level ones. A
  project's `.qwen/settings.json` cannot add or redirect a Live Voice route.
- A **bare** `modelId` that matches no `realtimeOnly` route uses the
  free-standing `experimental.liveVoice.endpoint` and
  `experimental.liveVoice.apiKey` fields as before, so existing setups keep
  working unchanged. A `provider:modelId` selector whose provider exists but
  has no such route is an error instead: a deleted or mistyped route never
  falls back to a stored key silently.
- While a route is selected, `experimental.liveVoice.apiKey` and
  `experimental.liveVoice.endpoint` are unused, and the setup API refuses to
  store new ones.

On the free-standing path the Web Shell's Qwen Live settings set the endpoint
and model directly. Enter the OpenAI-compatible base URL of the region or
dedicated domain your key belongs to, such as
`https://dashscope-intl.aliyuncs.com/compatible-mode/v1`; left empty, Live uses
Beijing (`https://dashscope.aliyuncs.com/compatible-mode/v1`), and a key from
elsewhere is rejected with `401`. The base URL is stored as entered and
converted to `wss://<host>/api-ws/v1/realtime` when a call starts; the host must
be a DashScope or `*.maas.aliyuncs.com` endpoint. While Live Voice is on, a new
endpoint is checked with a Realtime handshake before it is saved.

## Override reasoning capabilities

Set `capabilities.reasoning` on a model entry to override its reasoning format,
offered effort tiers and default. Known models inherit omitted fields from the
provider catalog at the selected endpoint; for example,
`"capabilities": { "reasoning": { "defaultEffort": "medium" } }` makes a
DashScope `qwen3.8-max` route use medium when no explicit effort is selected.
For an unknown alias, declare all three fields:

```json
{
  "id": "company-model-v2",
  "baseUrl": "https://gateway.example.com/v1",
  "envKey": "COMPANY_MODEL_API_KEY",
  "capabilities": {
    "reasoning": {
      "profile": "openai-effort",
      "efforts": ["low", "medium", "high"],
      "defaultEffort": "medium"
    }
  }
}
```

`efforts` replaces the supported subset of `low/medium/high/xhigh/max`.
An explicit default must belong to that subset. Profiles reuse existing formats:
Chat accepts `openai-effort`, `openai-reasoning`, `deepseek-openai`,
`dashscope-effort`, `dashscope-thinking` and `qwen-chat-template`;
Responses accepts `openai-reasoning`; Anthropic accepts `anthropic-manual`,
`anthropic-adaptive` and `deepseek-anthropic`; Gemini/Vertex uses `gemini`.
The two toggle-only profiles omit `efforts` and `defaultEffort`. Gemini uses
low/medium/high. An adaptive profile selects adaptive thinking even when a
legacy manual budget is configured. Existing complete capability declarations
continue to work.

Reasoning changes apply before the next user prompt. Its requests, retries and
child agents share the captured reasoning configuration. Invalid updates retain
the previous configuration and log the model/field error. Invalid declarations
in fresh sessions safely fall back to the existing model behavior. Explicit user choices
and provider-native overrides in `reasoning`, `samplingParams` and `extra_body`
retain their existing precedence; a model default is not saved as a user choice.
This mechanism does not change endpoint, credential or image-model lifecycles.

## Configuration Examples by Auth Type

Below are comprehensive configuration examples for different authentication types, showing the available parameters and their combinations.

### Supported Auth Types

Use one of the built-in provider ids below, or map a custom id with `providerProtocol`:

| Effective protocol | Description                                                                                                                                     |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `openai`           | OpenAI-compatible APIs. Defaults to Chat Completions; set a model's `wireApi` to `responses` for the Responses API.                             |
| `anthropic`        | Anthropic Claude API                                                                                                                            |
| `gemini`           | Google Gemini API                                                                                                                               |
| `qwen-oauth`       | Qwen OAuth (hard-coded, cannot be overridden in `modelProviders`)                                                                               |
| `vertex-ai`        | Google Vertex AI (uses the `gemini` protocol and the `@google/genai` SDK in Vertex AI mode; selecting it sets `GOOGLE_GENAI_USE_VERTEXAI=true`) |

> [!note]
> Vertex AI entries can authenticate with **Application Default Credentials**. Set `GOOGLE_CLOUD_PROJECT` (and optionally `GOOGLE_CLOUD_LOCATION`, which defaults to `global`) and leave `envKey` unset, along with every other key source the resolver reads: `GOOGLE_API_KEY`, `settings.security.auth.apiKey`, and the CLI key flags. Any API key value that reaches a Vertex entry switches the Google SDK to Vertex Express mode, which ignores the project, the location and your ADC credentials. An entry that declares an `envKey` is never routed to ADC, so a key that fails to be injected keeps failing on that variable instead of silently authenticating as a different principal.

> [!warning]
> A provider id that is neither a built-in protocol nor mapped via `providerProtocol` (e.g. a typo like `"openai-custom"`) cannot be routed, so its whole entry is **skipped** with a warning — its models simply won't appear in the `/model` picker. Use one of the supported auth type values above for built-in providers, or add a [`providerProtocol`](#custom-provider-ids-providerprotocol) mapping for a custom id.

### Custom provider ids (`providerProtocol`)

Built-in provider ids (`openai`, `gemini`, `anthropic`, `vertex-ai`, `qwen-oauth`) are routed to their SDK protocol automatically. To use a **custom** provider id — for example to group several OpenAI-compatible endpoints under a friendlier name — declare it under `modelProviders` and map it to a built-in protocol with the top-level `providerProtocol` setting:

```json
{
  "modelProviders": {
    "idealab": [
      {
        "id": "my-model",
        "envKey": "IDEALAB_API_KEY",
        "baseUrl": "https://idealab.example.com/v1"
      }
    ]
  },
  "providerProtocol": {
    "idealab": "openai"
  }
}
```

Without a matching `providerProtocol` entry, a custom provider id is skipped (see the warning above).

### Selecting the OpenAI API

Set `wireApi` beside `id`, `envKey`, and `baseUrl` on an OpenAI-compatible model:

```json
{
  "modelProviders": {
    "openai": [
      {
        "id": "my-model",
        "wireApi": "responses",
        "envKey": "OPENAI_API_KEY",
        "baseUrl": "https://api.openai.com/v1"
      }
    ]
  },
  "security": { "auth": { "selectedType": "openai" } },
  "model": { "name": "my-model" }
}
```

The supported values are `chat-completions` and `responses`. Omitting `wireApi` uses Chat Completions for `openai`, including custom providers mapped to `openai`. Other values, or `wireApi` on an Anthropic, Gemini, Vertex AI, or Qwen OAuth model, are configuration errors.

Use `openai` with per-model `wireApi` for new configurations. The `modelProviders.openai-responses` format and `providerProtocol` mappings to `openai-responses` released in v0.23.3 remain readable. Explicit provider mappings take precedence over bucket names; explicit `wireApi` takes precedence over either OpenAI protocol. Loading does not rewrite settings or change credential references. Reconfiguration writes the selected routes in the new format and removes only their matching old entries in the writable scope; entries under a provider id that contains a dot are left in place, and unrelated models, endpoints, APIs and scopes remain unchanged. `api` is not an alias for `wireApi`.

`wireApi` is local routing metadata; it does not belong in `generationConfig` or `extra_body` and is not sent in the request body. New custom setup shares one credential slot for the same OpenAI endpoint across both APIs, so rotating that key updates both routes. Manually configured models can use distinct explicit `envKey` references when independent credentials are needed. In `/auth` → Custom Provider, select OpenAI-compatible and then the API format.

At startup, `selectedType: "openai"` can resolve a model explicitly configured with `wireApi: "responses"` when there is no matching Chat route at the selected configured endpoint. The model picker and recorded sessions retain the effective protocol (`openai` or `openai-responses`) so both routes can be selected and resumed independently. No endpoint detection or automatic fallback occurs when an API request fails.

### Transports Used for API Requests

The effective model protocol determines the transport. Both OpenAI APIs use the `openai` provider group; `wireApi` selects Chat Completions or Responses:

| Effective protocol | Transport                                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `openai`           | [`openai`](https://www.npmjs.com/package/openai) - Official OpenAI Node.js SDK                                     |
| `openai-responses` | Direct HTTP/SSE calls to `/v1/responses` (no SDK); embeddings use [`openai`](https://www.npmjs.com/package/openai) |
| `anthropic`        | [`@anthropic-ai/sdk`](https://www.npmjs.com/package/@anthropic-ai/sdk) - Official Anthropic SDK                    |
| `gemini`           | [`@google/genai`](https://www.npmjs.com/package/@google/genai) - Official Google GenAI SDK                         |
| `qwen-oauth`       | [`openai`](https://www.npmjs.com/package/openai) with custom provider (DashScope-compatible)                       |

This means the `baseUrl` you configure should be compatible with the corresponding transport's expected API format. For example, `wireApi: "responses"` requires a Responses-compatible endpoint.

### OpenAI-compatible providers (`openai`)

This auth type supports not only OpenAI's official API but also any OpenAI-compatible endpoint, including aggregated model providers like OpenRouter and Requesty.

```
/* Detailed source-code truncated for AI context efficiency. */
```

When pointing an entry at a hosted OpenAI-compatible gateway, set `baseUrl` to the API's `/v1` root (for example, `https://gateway.example.com/v1`) rather than the full `/v1/chat/completions` path — the SDK appends the request path itself.

### OpenAI Responses API (`openai-responses`)

Use `openai` with `wireApi: "responses"` to target OpenAI's `/v1/responses` endpoint. When the endpoint returns encrypted reasoning with visible thought text, it replays prior-turn reasoning across turns and `--resume` via `reasoning.encrypted_content`. Compatible endpoints that stream `response.reasoning_text.delta` also display their reasoning, but endpoints without `encrypted_content` cannot replay the opaque reasoning state. Use `reasoning.effort` (not `extra_body.enable_thinking`, which the Chat Completions wires use) to control reasoning intensity.

```json
{
  "env": {
    "OPENAI_API_KEY": "sk-your-actual-openai-key-here"
  },
  "modelProviders": {
    "openai": [
      {
        "id": "gpt-5.1",
        "wireApi": "responses",
        "name": "GPT-5.1 (Responses API)",
        "envKey": "OPENAI_API_KEY",
        "baseUrl": "https://api.openai.com/v1",
        "generationConfig": {
          "timeout": 60000,
          "reasoning": {
            "effort": "high"
          },
          "samplingParams": {
            "temperature": 0.7,
            "max_tokens": 4096
          }
        }
      }
    ]
  }
}
```

> [!note]
> `extra_body` on this wire is fill-only: a key is written to the request body only when the generated request has no value for it, so it cannot override a field the pipeline already set (`model`, `input`, `reasoning`, `temperature`, `max_output_tokens`, ...). The legacy `enable_thinking` key is the one exception to even that — it is removed rather than forwarded (it isn't a Responses API field), and translated into `reasoning.effort: "medium"` when no explicit `reasoning` is set. Set `reasoning.effort` directly instead of `extra_body.enable_thinking` for this provider.

### Anthropic (`anthropic`)

```json
{
  "env": {
    "ANTHROPIC_API_KEY": "sk-ant-your-actual-anthropic-key-here"
  },
  "modelProviders": {
    "anthropic": [
      {
        "id": "claude-3-5-sonnet",
        "name": "Claude 3.5 Sonnet",
        "envKey": "ANTHROPIC_API_KEY",
        "baseUrl": "https://api.anthropic.com/v1",
        "generationConfig": {
          "timeout": 120000,
          "maxRetries": 3,
          "contextWindowSize": 200000,
          "samplingParams": {
            "temperature": 0.7,
            "max_tokens": 8192,
            "top_p": 0.9
          }
        }
      },
      {
        "id": "claude-3-opus",
        "name": "Claude 3 Opus",
        "envKey": "ANTHROPIC_API_KEY",
        "baseUrl": "https://api.anthropic.com/v1",
        "generationConfig": {
          "timeout": 180000,
          "samplingParams": {
            "temperature": 0.3,
            "max_tokens": 4096
          }
        }
      }
    ]
  }
}
```

### Google Gemini (`gemini`)

```json
{
  "env": {
    "GEMINI_API_KEY": "AIza-your-actual-gemini-key-here"
  },
  "modelProviders": {
    "gemini": [
      {
        "id": "gemini-2.0-flash",
        "name": "Gemini 2.0 Flash",
        "envKey": "GEMINI_API_KEY",
        "baseUrl": "https://generativelanguage.googleapis.com",
        "capabilities": {
          "vision": true
        },
        "generationConfig": {
          "timeout": 60000,
          "maxRetries": 2,
          "contextWindowSize": 1000000,
          "schemaCompliance": "auto",
          "samplingParams": {
            "temperature": 0.4,
            "top_p": 0.95,
            "max_tokens": 8192,
            "top_k": 40
          }
        }
      }
    ]
  }
}
```

For a vision model that can also follow the normal Qwen Code agent policy and use tools, opt in to full-turn image routing with both capabilities:

```json
"capabilities": {
  "vision": true,
  "agent": true
}
```

When a text-only primary uses that model as its configured vision fallback, the complete image-bearing turn stays on that exact provider, model, and endpoint across tool calls and retries. The next independent turn returns to the primary, and each model request receives only media modalities supported by its target. Omit `agent` (or set it to `false`) to keep the safer Vision Bridge transcription flow.

### Local Self-Hosted Models (via OpenAI-compatible API)

Most local inference servers (vLLM, Ollama, LM Studio, etc.) provide an OpenAI-compatible API endpoint. Configure them using the `openai` auth type with a local `baseUrl`:

```json
{
  "env": {
    "OLLAMA_API_KEY": "ollama",
    "VLLM_API_KEY": "not-needed",
    "LMSTUDIO_API_KEY": "lm-studio"
  },
  "modelProviders": {
    "openai": [
      {
        "id": "qwen2.5-7b",
        "name": "Qwen2.5 7B (Ollama)",
        "envKey": "OLLAMA_API_KEY",
        "baseUrl": "http://localhost:11434/v1",
        "generationConfig": {
          "timeout": 300000,
          "streamIdleTimeoutMs": 600000,
          "maxRetries": 1,
          "contextWindowSize": 32768,
          "samplingParams": {
            "temperature": 0.7,
            "top_p": 0.9,
            "max_tokens": 4096
          }
        }
      },
      {
        "id": "llama-3.1-8b",
        "name": "Llama 3.1 8B (vLLM)",
        "envKey": "VLLM_API_KEY",
        "baseUrl": "http://localhost:8000/v1",
        "generationConfig": {
          "timeout": 120000,
          "maxRetries": 2,
          "contextWindowSize": 128000,
          "samplingParams": {
            "temperature": 0.6,
            "max_tokens": 8192
          }
        }
      },
      {
        "id": "local-model",
        "name": "Local Model (LM Studio)",
        "envKey": "LMSTUDIO_API_KEY",
        "baseUrl": "http://localhost:1234/v1",
        "generationConfig": {
          "timeout": 60000,
          "samplingParams": {
            "temperature": 0.5
          }
        }
      }
    ]
  }
}
```

For queued or slow local OpenAI-compatible servers, `streamIdleTimeoutMs`
controls how long this model may stay silent between streamed chunks. It
overrides the global `QWEN_STREAM_IDLE_TIMEOUT_MS` value for the selected
provider entry; set it to `0` to disable the idle guard. The separate 15-minute
stream lifetime cap still applies unless `QWEN_STREAM_MAX_LIFETIME_MS` is raised
or disabled.

For local servers that don't require authentication, you can use any placeholder value for the API key:

```bash
# For Ollama (no auth required)
export OLLAMA_API_KEY="ollama"

# For vLLM (if no auth is configured)
export VLLM_API_KEY="not-needed"
```

> [!note]
>
> The `extra_body` parameter is **only supported for OpenAI-compatible providers** (`openai`, `qwen-oauth`). It is ignored for Anthropic, and Gemini providers. On `openai-responses` the `enable_thinking` key is translated rather than forwarded — see the [OpenAI Responses API](#openai-responses-api-openai-responses) note.

> [!note]
>
> **About `envKey`**: The `envKey` field specifies the **name of an environment variable**, not the actual API key value. For the configuration to work, you need to ensure the corresponding environment variable is set with your real API key. There are two ways to do this:
>
> - **Option 1: Using a `.env` file** (recommended for security):
>   ```bash
>   # ~/.qwen/.env (or project root)
>   OPENAI_API_KEY=sk-your-actual-key-here
>   ```
>   Be sure to add `.env` to your `.gitignore` to prevent accidentally committing secrets.
> - **Option 2: Using the `env` field in `settings.json`** (as shown in the examples above):
>   ```json
>   {
>     "env": {
>       "OPENAI_API_KEY": "sk-your-actual-key-here"
>     }
>   }
>   ```
>
> Each provider example includes an `env` field to illustrate how the API key should be configured.

## Alibaba Cloud Coding Plan

Alibaba Cloud Coding Plan provides a pre-configured set of Qwen models optimized for coding tasks. This feature is available for users with Alibaba Cloud Coding Plan API access and offers a simplified setup experience with automatic model configuration updates.

### Overview

When you authenticate with an Alibaba Cloud Coding Plan API key using the `/auth` command, Qwen Code automatically configures the following models:

| Model ID               | Name                 | Description                                               |
| ---------------------- | -------------------- | --------------------------------------------------------- |
| `qwen3.5-plus`         | qwen3.5-plus         | Advanced model with thinking enabled                      |
| `qwen3.6-plus`         | qwen3.6-plus         | Latest model with thinking enabled (Pro subscribers only) |
| `qwen3.7-plus`         | qwen3.7-plus         | Advanced model with thinking enabled                      |
| `qwen3-coder-plus`     | qwen3-coder-plus     | Optimized for coding tasks                                |
| `qwen3-coder-next`     | qwen3-coder-next     | Experimental coding model                                 |
| `qwen3-max-2026-01-23` | qwen3-max-2026-01-23 | Latest max model with thinking enabled                    |
| `glm-5`                | glm-5                | GLM model with thinking enabled                           |
| `glm-4.7`              | glm-4.7              | GLM model with thinking enabled                           |
| `kimi-k2.5`            | kimi-k2.5            | Kimi model with thinking and vision/video support         |
| `MiniMax-M2.5`         | MiniMax-M2.5         | MiniMax model with thinking enabled                       |

### Setup

1. Obtain an Alibaba Cloud Coding Plan API key:
   - **China**: <https://bailian.console.aliyun.com/?tab=model#/efm/coding_plan>
   - **International**: <https://modelstudio.console.alibabacloud.com/?tab=dashboard#/efm/coding_plan>
2. Run the `/auth` command in Qwen Code
3. Select **Alibaba ModelStudio**, then choose **Coding Plan** from the sub-menu
4. Select your region
5. Enter your API key when prompted

The models will be automatically configured and added to your `/model` picker.

### Regions

Alibaba Cloud Coding Plan supports two regions:

| Region               | Endpoint                                        | Description             |
| -------------------- | ----------------------------------------------- | ----------------------- |
| China                | `https://coding.dashscope.aliyuncs.com/v1`      | Mainland China endpoint |
| Global/International | `https://coding-intl.dashscope.aliyuncs.com/v1` | International endpoint  |

The region is selected during authentication and stored in `settings.json` under the `modelProviders` configuration. To switch regions, re-run the `/auth` command and select a different region.

### API Key Storage

When you configure Coding Plan through the `/auth` command, the API key is stored using the reserved environment variable name `BAILIAN_CODING_PLAN_API_KEY`. By default, it is stored in the `env` field of your `settings.json` file.

> [!warning]
>
> **Security Recommendation**: For better security, it is recommended to move the API key from `settings.json` to a separate `.env` file and load it as an environment variable. For example:
>
> ```bash
> # ~/.qwen/.env
> BAILIAN_CODING_PLAN_API_KEY=your-api-key-here
> ```
>
> Then ensure this file is added to your `.gitignore` if you're using project-level settings.

### Automatic Updates

Coding Plan model configurations are versioned. When Qwen Code detects a newer version of the model template, you will be prompted to update. Accepting the update will:

- Replace the existing Coding Plan model configurations with the latest versions
- Preserve any custom model configurations you've added manually
- Leave your selected model unchanged; if it is no longer in the updated
  configuration, use `/model` to choose a new one

The update process refreshes the model configurations and features without
changing your selected model.

### Manual Configuration (Advanced)

If you prefer to manually configure Coding Plan models, you can add them to your `settings.json` like any OpenAI-compatible provider:

```json
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3-coder-plus",
        "name": "qwen3-coder-plus",
        "description": "Qwen3-Coder via Alibaba Cloud Coding Plan",
        "envKey": "YOUR_CUSTOM_ENV_KEY",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1"
      }
    ]
  }
}
```

> [!note]
>
> When using manual configuration:
>
> - You can use any environment variable name for `envKey`
> - You do not need to configure `codingPlan.*`
> - **Automatic updates will not apply** to manually configured Coding Plan models

> [!warning]
>
> If you also use automatic Coding Plan configuration, automatic updates may overwrite your manual configurations if they use the same `envKey` and `baseUrl` as the automatic configuration. To avoid this, ensure your manual configuration uses a different `envKey` if possible.

## Resolution Layers and Atomicity

The effective auth/model/credential values are chosen per field using the following precedence (first present wins). You can combine `--auth-type` with `--model` to point directly at a provider entry; these CLI flags run before other layers.

| Layer (highest → lowest)   | authType                            | model                                           | apiKey                                            | baseUrl                                            | apiKeyEnvKey           | proxy                             |
| -------------------------- | ----------------------------------- | ----------------------------------------------- | ------------------------------------------------- | -------------------------------------------------- | ---------------------- | --------------------------------- |
| Programmatic overrides     | `/auth`                             | `/auth` input                                   | `/auth` input                                     | `/auth` input                                      | —                      | —                                 |
| Model provider selection   | —                                   | `modelProvider.id`                              | `env[modelProvider.envKey]`                       | `modelProvider.baseUrl`                            | `modelProvider.envKey` | —                                 |
| CLI arguments              | `--auth-type`                       | `--model`                                       | `--openai-api-key`                                | `--openai-base-url`                                | —                      | —                                 |
| Environment variables      | —                                   | Provider-specific mapping (e.g. `OPENAI_MODEL`) | Provider-specific mapping (e.g. `OPENAI_API_KEY`) | Provider-specific mapping (e.g. `OPENAI_BASE_URL`) | —                      | —                                 |
| Settings (`settings.json`) | `security.auth.selectedType`        | `model.name`                                    | `security.auth.apiKey`                            | `security.auth.baseUrl`                            | —                      | —                                 |
| Default / computed         | Falls back to `AuthType.QWEN_OAUTH` | Built-in default (OpenAI ⇒ `qwen3.5-plus`)      | —                                                 | —                                                  | —                      | `Config.getProxy()` if configured |

\*When present, CLI auth flags override settings. Otherwise, `security.auth.selectedType` or the implicit default determine the auth type. Qwen OAuth and OpenAI are the only auth types surfaced without extra configuration.

> [!note]
>
> `--openai-api-key` and `--openai-base-url` are the only credential CLI flags. They apply to the active OpenAI-compatible provider regardless of its name — there are no `--anthropic-*` / `--gemini-*` credential flags. Provider-specific credentials that aren't passed on the CLI are resolved from environment variables (see the row below).

> [!warning]
>
> **Deprecation of `security.auth.apiKey` and `security.auth.baseUrl`:** Directly configuring API credentials via `security.auth.apiKey` and `security.auth.baseUrl` in `settings.json` is deprecated. These settings were used in historical versions for credentials entered through the UI, but the credential input flow was removed in version 0.10.1. These fields will be fully removed in a future release. **It is strongly recommended to migrate to `modelProviders`** for all model and credential configurations. Use `envKey` in `modelProviders` to reference environment variables for secure credential management instead of hardcoding credentials in settings files.

## Generation Config Layering: The Impermeable Provider Layer

The configuration resolution follows a strict layering model with one crucial rule: **the modelProvider layer is impermeable**.

### How it works

1. **When a modelProvider model IS selected** (e.g., via `/model` command choosing a provider-configured model):
   - The entire `generationConfig` from the provider is applied **atomically**
   - **The provider layer is completely impermeable** — lower layers (CLI, env, settings) do not participate in generationConfig resolution at all
   - All fields defined in `modelProviders[].generationConfig` use the provider's values
   - All fields **not defined** by the provider are set to `undefined` (not inherited from settings)
   - This ensures provider configurations act as a complete, self-contained "sealed package"

   If a model is listed in `modelProviders`, put all model-specific
   generation settings for that model in the matching provider entry. Top-level
   `model.generationConfig` values, including `contextWindowSize`,
   `modalities`, `customHeaders`, and `extra_body`, are ignored for provider
   models. Configure those fields under
   `modelProviders[authType][].generationConfig` for them to apply.

2. **When NO modelProvider model is selected** (e.g., using `--model` with a raw model ID, or using CLI/env/settings directly):
   - The resolution falls through to lower layers
   - Fields are populated from CLI → env → settings → defaults
   - This creates a **Runtime Model** (see next section)

### Per-field precedence for `generationConfig`

| Priority | Source                                        | Behavior                                                                                                 |
| -------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| 1        | Programmatic overrides                        | Runtime `/model`, `/auth` changes                                                                        |
| 2        | `modelProviders[authType][].generationConfig` | **Impermeable layer** - completely replaces all generationConfig fields; lower layers do not participate |
| 3        | `settings.model.generationConfig`             | Only used for **Runtime Models** (when no provider model is selected)                                    |
| 4        | Content-generator defaults                    | Provider-specific defaults (e.g., OpenAI vs Gemini) - only for Runtime Models                            |

### Dynamic values in `customHeaders`

A `customHeaders` value may contain the placeholder `${session_id}`, which is
expanded per request with the current Qwen Code session ID. Use it for gateways
that require a stable per-conversation identifier — OpenCode Go, for example,
rejects requests without `x-opencode-session`:

```json
{
  "generationConfig": {
    "customHeaders": {
      "x-opencode-session": "${session_id}"
    }
  }
}
```

Because the value is resolved per request rather than baked into the SDK client,
`/new` and `/resume` rotate it without a restart.

⚠️ **Two steps are required.** The provider entry above is only half of it — a
placeholder is inert until you also switch on
[`outboundCorrelation.allowDynamicHeaderValues`](settings.md#outboundcorrelation):

```json
{
  "outboundCorrelation": {
    "allowDynamicHeaderValues": true
  }
}
```

Until you do, a value containing a placeholder is **dropped** rather than sent,
and Qwen Code prints a warning at startup naming the header and this setting.
The header is never sent with a literal `${session_id}` in it.

The switch is global because it is a consent decision, separate from _where_ the
value goes: an expanded value carries live session state to whoever receives it,
and the switch controls only whether `${session_id}` may be expanded. It does not
identify which settings source supplied the header.

**Privacy note:** the session ID is a stable identifier for the life of a
conversation, so any host you send it to can group every request of that
conversation. Which hosts those are is decided by which provider entries carry
the header — there is no separate host list to keep in sync with your `baseUrl`.

### Atomic field treatment

The following fields are treated as atomic objects - provider values completely replace the entire object, no merging occurs:

- `samplingParams` - Temperature, top_p, max_tokens, etc.
- `customHeaders` - Custom HTTP headers (may contain `${session_id}`; see [Dynamic values](#dynamic-values-in-customheaders))
- `extra_body` - Extra request body parameters

### Example

```jsonc
// User settings (~/.qwen/settings.json)
{
  "model": {
    "generationConfig": {
      "timeout": 30000,
      "samplingParams": { "temperature": 0.5, "max_tokens": 1000 }
    }
  }
}

// modelProviders configuration
{
  "modelProviders": {
    "openai": [{
      "id": "gpt-4o",
      "envKey": "OPENAI_API_KEY",
      "generationConfig": {
        "timeout": 60000,
        "samplingParams": { "temperature": 0.2 }
      }
    }]
  }
}
```

When `gpt-4o` is selected from modelProviders:

- `timeout` = 60000 (from provider, overrides settings)
- `samplingParams.temperature` = 0.2 (from provider, completely replaces settings object)
- `samplingParams.max_tokens` = **undefined** (not defined in provider, and provider layer does not inherit from settings — fields are explicitly set to undefined if not provided)

When using a raw model via `--model gpt-4` (not from modelProviders, creates a Runtime Model):

- `timeout` = 30000 (from settings)
- `samplingParams.temperature` = 0.5 (from settings)
- `samplingParams.max_tokens` = 1000 (from settings)

The merge strategy for `modelProviders` itself is REPLACE: the entire `modelProviders` from project settings will override the corresponding section in user settings, rather than merging the two.

## Reasoning / thinking configuration

The optional `reasoning` field under `generationConfig` controls how aggressively the model reasons before responding. The Anthropic and Gemini converters always honor it. The OpenAI-compatible pipeline honors it **unless** `generationConfig.samplingParams` is set. Known GPT-5 models and GPT-6 Astra are an exception: unrelated sampling keys do not suppress configured effort. See "Interaction with `samplingParams`" below.

```jsonc
{
  "modelProviders": {
    "openai": [
      {
        "id": "deepseek-v4-pro",
        "name": "DeepSeek V4 Pro",
        "baseUrl": "https://api.deepseek.com/v1",
        "envKey": "DEEPSEEK_API_KEY",
        "generationConfig": {
          // The effort tiers:
          //   'low'    — lightest reasoning
          //   'medium' | 'xhigh' — mapped to 'high' on DeepSeek
          //   'high'   — default reasoning intensity
          //   'max'    — DeepSeek-specific extra-strong tier
          // Or set `false` to disable reasoning entirely.
          "reasoning": { "effort": "max" },
        },
      },
    ],
  },
}
```

### Per-provider behavior

| Protocol / provider                           | Wire shape                                                                               | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **OpenAI / DashScope** (`qwen3.8-max` family) | Flat `reasoning_effort: <effort>` body parameter                                         | The `/effort` tiers are passed through for any model id starting with `qwen3.8-max` (including dated snapshots and `-latest` aliases); DashScope applies any model-specific mapping. This family's ladder stops at `xhigh`, so a configured `max` is clamped to `xhigh` (logged once) rather than sent and rejected. An explicit `reasoning_effort` in `samplingParams` or `extra_body` is a verbatim override and is not clamped. When `reasoning_effort` and `thinking_budget` conflict, the normal `extra_body` > `samplingParams` > `reasoning` precedence keeps only the higher-priority field; an explicit same-layer pair keeps `reasoning_effort`, matching the provider's behavior before cross-layer resolution. If a static field wins, `/effort` reports that field instead of implying the requested tier is effective. When an effort tier wins, a conflicting `enable_thinking` is also dropped. An explicit `enable_thinking: false` in `extra_body` is honoured rather than dropped: it overrides the configured tier as `reasoning_effort: 'none'`, one of the few places `extra_body` does not win verbatim. Other Qwen models continue to map a selected effort to `enable_thinking: true`; a `reasoning_effort` override passes through there unless it conflicts with a `thinking_budget` (a pair DashScope rejects), in which case the inert `reasoning_effort` is dropped and both `enable_thinking` and `thinking_budget` survive. |
| **OpenAI / DeepSeek** (`api.deepseek.com`)    | Flat `reasoning_effort: <effort>` body parameter                                         | When `reasoning.effort` is set in the nested config shape, it's rewritten to flat `reasoning_effort`; for the built-in DeepSeek models, `'low'`, `'high'` and `'max'` pass through and `'medium'`/`'xhigh'` are normalized to `'high'` — mirroring DeepSeek's [effort mapping](https://api-docs.deepseek.com/guides/thinking_mode). Top-level `samplingParams.reasoning_effort` or `extra_body.reasoning_effort` overrides skip this normalization and ship verbatim. `max` is accepted only on a real DeepSeek hostname; a `deepseek`-named model on another host keeps the generic `xhigh` ceiling, matching the hostname gate on the reshape itself.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| **OpenAI / Z.ai** (`z.ai`, `bigmodel.cn`)     | Flat `reasoning_effort: <effort>` body parameter                                         | GLM-5.2+ on a Z.ai host takes the full ladder, `max` included, and the nested `reasoning.effort` is rewritten to the flat field. Older GLM ids, and a `glm-*` model reached on any other host, keep the generic `xhigh` ceiling: the model name alone says nothing about what that endpoint accepts.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| **OpenAI** (other compatible servers)         | Known GPT-5 / GPT-6 Astra: flat `reasoning_effort`; other models: nested `reasoning`     | GPT effort is clamped in both directions to the supported subset for the known model. GPT-5.6 and GPT-6 Astra allow `max`, while earlier models have lower ceilings. Tiers below the model floor are raised: GPT-5 Pro accepts only `high`; GPT-5.2 Pro, GPT-5.4 Pro and GPT-5.5 Pro raise `low` to `medium`. OpenRouter retains nested `reasoning`. Unknown model names keep the generic `xhigh` ceiling and nested shape. Explicit reasoning values in `samplingParams` / `extra_body` bypass the configured-tier clamp.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| **OpenAI Responses** (`openai-responses`)     | `reasoning: { effort, summary: "auto" }` plus `include: ["reasoning.encrypted_content"]` | Every tier passes through verbatim with no clamping. `extra_body.enable_thinking: true` is translated to `reasoning: { effort: "medium" }` when no explicit `reasoning` is set (and is never itself forwarded — it has no meaning on this wire); prefer setting `reasoning.effort` directly.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| **Anthropic** (real `api.anthropic.com`)      | `output_config: { effort }` plus the `effort-2025-11-24` beta header                     | Real Anthropic accepts `'low'`/`'medium'`/`'high'` only. `'max'` is **clamped to `'high'`** with a `debugLogger.warn` line (once per generator); if you want max effort, switch the baseURL to a DeepSeek-compatible endpoint that supports it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| **Anthropic** (`api.deepseek.com/anthropic`)  | Same `output_config: { effort }` + beta header                                           | `'low'`, `'high'` and `'max'` pass through unchanged; `'medium'`/`'xhigh'` are mapped to `'high'`, matching DeepSeek's server-side mapping.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| **Gemini** (`@google/genai`)                  | `thinkingConfig: { includeThoughts: true, thinkingLevel }`                               | `'low'` → `LOW`, `'high'`/`'max'` → `HIGH`, others → `THINKING_LEVEL_UNSPECIFIED` (Gemini has no `MAX` tier).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

### `reasoning: false`

Setting `reasoning: false` (the literal boolean) explicitly disables thinking on models that support disabling — useful for cheap side queries that don't benefit from reasoning. This is honored at the request level too via `request.config.thinkingConfig.includeThoughts: false` for one-off calls (e.g. suggestion generation).

On a `api.deepseek.com` baseURL, the OpenAI pipeline emits the explicit `thinking: { type: 'disabled' }` field that DeepSeek V4+ requires — the server-side default is `'enabled'`, so simply omitting `reasoning_effort` would still pay thinking latency/cost. Self-hosted DeepSeek backends (sglang/vllm) and other OpenAI-compatible servers do **not** receive this field; if you need to disable thinking on those, inject `thinking: { type: 'disabled' }` (or whatever knob your inference framework exposes) via `samplingParams`/`extra_body`.

For known GPT models that allow disabling, non-OpenRouter endpoints receive `reasoning_effort: 'none'`; OpenRouter receives nested `reasoning: { enabled: false }` instead. Mandatory-thinking models reject off in model controls and omit unsupported disable values from requests, so `reasoning: false` cannot turn their thinking off. An explicit model reasoning capability takes precedence over the built-in tier list and selects the native disable field; OpenRouter keeps its provider-level disable behavior. The built-in mandatory set is `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-5-pro`, `gpt-5.1-codex`, `gpt-5.1-codex-max`, `gpt-5.2-codex`, `gpt-5.3-codex`, `gpt-5.2-pro`, `gpt-5.4-pro`, `gpt-5.5-pro`, and `gpt-6-astra`.

On an `openrouter.ai` baseURL, the OpenAI pipeline emits OpenRouter's provider-level `reasoning: { enabled: false }` field when reasoning is disabled. Mandatory-thinking models do not receive this disable field. Other OpenAI-compatible servers do not automatically receive this OpenRouter-specific field; use their native disable knob.

### Interaction with `samplingParams` (OpenAI-compatible only)

> [!warning]
>
> Except for known GPT models and models with explicit reasoning capabilities, when `generationConfig.samplingParams` is set on an OpenAI-compatible provider, the pipeline ships those keys to the wire **verbatim** and skips the separate `reasoning` injection entirely. So a config like `{ samplingParams: { temperature: 0.5 }, reasoning: { effort: 'max' } }` will silently drop the reasoning field on OpenAI/DeepSeek requests. A `reasoning` object placed inside `samplingParams` is your own value and ships unchanged while reasoning is enabled: the effort ceiling above applies only to the tier the pipeline injects from `/effort`.
>
> Known GPT-5 models and GPT-6 Astra keep configured effort alongside unrelated sampling keys. For example, `{ samplingParams: { temperature: 0.5 }, reasoning: { effort: 'max' } }` sends `temperature: 0.5` and flat `reasoning_effort: 'xhigh'` on GPT-5.4, or `'max'` on GPT-5.6 / GPT-6 Astra. On an `openrouter.ai` baseURL the same clamped tier ships as nested `reasoning: { effort }` instead. On non-OpenRouter endpoints, explicit flat reasoning overrides win; nullish or empty-string flat placeholders allow the configured tier. On OpenRouter, a sampling flat override suppresses configured nested effort unless explicit model capabilities inject it; an extra-body-only flat override does not replace the configured nested effort.
>
> DashScope Qwen models are another exception: their provider reads `reasoning` directly and maps it to `reasoning_effort` or `enable_thinking`. On the qwen3.8-max family, provider-specific `samplingParams` fields still take precedence when the wire parameters conflict; on older qwen hybrids, a configured effort tier collapses to `enable_thinking: true`, which overrides a `samplingParams.enable_thinking` value.
>
> For other models, include the provider's reasoning knob directly when using `samplingParams` — for DeepSeek that is `samplingParams.reasoning_effort`. Known GPT models map configured effort automatically; only add a raw override when intentionally bypassing that mapping. Raw nested `reasoning`, including `null`, remains a whole-object override while reasoning is enabled. Disabling via `reasoning: false` or request-level `includeThoughts: false` removes the nested value, including raw overrides in either layer. Non-OpenRouter GPT requests then send `reasoning_effort: 'none'` when disabling is allowed; OpenRouter uses its nested disable field instead. Mandatory-thinking models receive neither substitute. Outside OpenRouter its meaning depends on the gateway, so model controls show the model default. Any raw override that blocks a configured tier causes an explicit tier change to fail without saving a preference. The thinking switch restores configured raw defaults after disabling only when they permit thinking. If the raw state or configured reasoning default is off, the thinking switch cannot be turned on and the saved preference is retained. This also applies when explicit capabilities omit a default tier or expose only a thinking toggle. An explicit default command still resets the preference. Remove the blocking raw override to choose a different tier.
>
> The Anthropic and Gemini converters are unaffected — they always read `reasoning.effort` directly regardless of `samplingParams`.

### `budget_tokens`

You can pin an exact thinking-token budget by including `budget_tokens` alongside `effort`:

```jsonc
"reasoning": { "effort": "high", "budget_tokens": 50000 }
```

For Anthropic this becomes `thinking.budget_tokens`. For OpenAI/DeepSeek the field is preserved but currently ignored by the server — `reasoning_effort` is the load-bearing knob.

## Provider Models vs Runtime Models

Qwen Code distinguishes between two types of model configurations:

### Provider Model

- Defined in `modelProviders` configuration
- Has a complete, atomic configuration package
- When selected, its configuration is applied as an impermeable layer
- Appears in `/model` command list with full metadata (name, description, capabilities)
- Recommended for multi-model workflows and team consistency

### Runtime Model

- Created dynamically when using raw model IDs via CLI (`--model`), environment variables, or settings
- Not defined in `modelProviders`
- Configuration is built by "projecting" through resolution layers (CLI → env → settings → defaults)
- Automatically captured as a **RuntimeModelSnapshot** when a complete configuration is detected
- Allows reuse without re-entering credentials

### RuntimeModelSnapshot lifecycle

When you configure a model without using `modelProviders`, Qwen Code automatically creates a RuntimeModelSnapshot to preserve your configuration:

```bash
# This creates a RuntimeModelSnapshot with ID: $runtime|openai|my-custom-model
qwen --auth-type openai --model my-custom-model --openai-api-key $KEY --openai-base-url https://api.example.com/v1
```

The snapshot:

- Captures model ID, API key, base URL, and generation config
- Persists across sessions (stored in memory during runtime)
- Appears in the `/model` command list as a runtime option
- Can be switched to using `/model $runtime|openai|my-custom-model`

### Key differences

| Aspect                  | Provider Model                    | Runtime Model                              |
| ----------------------- | --------------------------------- | ------------------------------------------ |
| Configuration source    | `modelProviders` in settings      | CLI, env, settings layers                  |
| Configuration atomicity | Complete, impermeable package     | Layered, each field resolved independently |
| Reusability             | Always available in `/model` list | Captured as snapshot, appears if complete  |
| Team sharing            | Yes (via committed settings)      | No (user-local)                            |
| Credential storage      | Reference via `envKey` only       | May capture actual key in snapshot         |

### When to use each

- **Use Provider Models** when: You have standard models shared across a team, need consistent configurations, or want to prevent accidental overrides
- **Use Runtime Models** when: Quickly testing a new model, using temporary credentials, or working with ad-hoc endpoints

## Selection Persistence and Recommendations

> [!important]
>
> Define `modelProviders` in the user-scope `~/.qwen/settings.json` whenever possible and avoid persisting credential overrides in any scope. Keeping the provider catalog in user settings prevents merge/override conflicts between project and user scopes and ensures `/auth` and `/model` updates always write back to a consistent scope.

- `/model` and `/auth` persist `model.name` (where applicable) and `security.auth.selectedType` to the closest writable scope that already defines `modelProviders`; otherwise they fall back to the user scope. This keeps workspace/user files in sync with the active provider catalog.
- Without `modelProviders`, the resolver mixes CLI/env/settings layers, creating Runtime Models. This is fine for single-provider setups but cumbersome when frequently switching. Define provider catalogs whenever multi-model workflows are common so that switches stay atomic, source-attributed, and debuggable.

---

### Users Configuration Qwen Ignore (`docs/users/configuration/qwen-ignore.md`)

# Ignoring Files

This document provides an overview of the Qwen Ignore (`.qwenignore`) feature of Qwen Code. Qwen Code also recognizes custom ignore files configured by `context.fileFiltering.customIgnoreFiles`, which defaults to the compatibility files `.agentignore` and `.aiignore`.

Qwen Code includes the ability to automatically ignore files, similar to `.gitignore` (used by Git). Adding paths to `.qwenignore` or a configured custom ignore file will exclude them from tools that support this feature, although they will still be visible to other services (such as Git).

## How it works

When you add a path to one of these ignore files, tools that respect Qwen ignore rules will exclude matching files and directories from their operations. For example, when you use the [`read_many_files`](../../developers/tools/multi-file) command, any paths in `.qwenignore` or configured custom ignore files will be automatically excluded.

For the most part, these ignore files follow the conventions of `.gitignore` files:

- Blank lines and lines starting with `#` are ignored.
- Standard glob patterns are supported (such as `*`, `?`, and `[]`).
- Putting a `/` at the end will only match directories.
- Putting a `/` at the beginning anchors the path relative to the ignore file.
- `!` negates a pattern.

You can update these ignore files at any time. To apply the changes, you must restart your Qwen Code session.

## How to use ignore files

| Step                    | Description                                                                                                                                   |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Enable ignore rules** | Create `.qwenignore`, a default custom file (`.agentignore` / `.aiignore`), or a configured custom ignore file in your project root directory |
| **Add ignore rules**    | Open the ignore file and add paths to ignore, example: `/archive/` or `apikeys.txt`                                                           |

By default, Qwen Code reads `.qwenignore`, `.agentignore`, and `.aiignore`.
To use a different custom ignore file, configure:

```json
{
  "context": {
    "fileFiltering": {
      "customIgnoreFiles": [".cursorignore"]
    }
  }
}
```

`.qwenignore` is always included when `context.fileFiltering.respectQwenIgnore`
is enabled. Custom ignore file paths are relative to the project root.

### Ignore file examples

You can use any supported ignore file to ignore directories and files:

```
# Exclude your /packages/ directory and all subdirectories
/packages/

# Exclude your apikeys.txt file
apikeys.txt
```

You can use wildcards in your ignore file with `*`:

```
# Exclude all .md files
*.md
```

Finally, you can exclude files and directories from exclusion with `!`:

```
# Exclude all .md files except README.md
*.md
!README.md
```

To remove paths from an ignore file, delete the relevant lines.

---

### Users Configuration Settings (`docs/users/configuration/settings.md`)

# Qwen Code Configuration

> [!tip]
>
> **Authentication / API keys:** Authentication (API Key, Alibaba Cloud Coding Plan) and auth-related environment variables (like `OPENAI_API_KEY`) are documented in **[Authentication](../configuration/auth)**.

> [!note]
>
> **Note on New Configuration Format**: The format of the `settings.json` file has been updated to a new, more organized structure. The old format will be migrated automatically.
> Qwen Code offers several ways to configure its behavior, including environment variables, command-line arguments, and settings files. This document outlines the different configuration methods and available settings.

## Tool execution sandbox

`tools.executionSandbox` is an operator-only Linux policy with required `filesystem: "read-only" | "workspace-write"` and `network: "open" | "closed"`, plus optional `backend: "auto" | "bwrap" | "landlock"`. `auto` prefers bwrap and can fall back to partial Landlock filesystem enforcement only when command networking is open. System policy replaces the complete User/SystemDefaults object; Workspace values are ignored. Bare/safe modes retain the policy and changing it requires restart. See [Sandbox](../features/sandbox.md#linux-tool-execution-sandbox) for configuration, scope, unsupported integrations, verification and migration from whole-CLI bwrap.

## Configuration layers

Configuration is applied in the following order of precedence (lower numbers are overridden by higher numbers):

| Level | Configuration Source   | Description                                                                     |
| ----- | ---------------------- | ------------------------------------------------------------------------------- |
| 1     | Default values         | Hardcoded defaults within the application                                       |
| 2     | System defaults file   | System-wide default settings that can be overridden by other settings files     |
| 3     | User settings file     | Global settings for the current user                                            |
| 4     | Project settings file  | Project-specific settings                                                       |
| 5     | System settings file   | System-wide settings that override all other settings files                     |
| 6     | Environment variables  | System-wide or session-specific variables, potentially loaded from `.env` files |
| 7     | Command-line arguments | Values passed when launching the CLI                                            |

## Settings files

Qwen Code uses JSON settings files for persistent configuration. There are four locations for these files:

| File Type             | Location                                                                                                                                                                                                                                                                        | Scope                                                                                                                                                                                                                     |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| System defaults file  | Linux: `/etc/qwen-code/system-defaults.json`<br>Windows: `C:\ProgramData\qwen-code\system-defaults.json`<br>macOS: `/Library/Application Support/QwenCode/system-defaults.json` <br>The path can be overridden using the `QWEN_CODE_SYSTEM_DEFAULTS_PATH` environment variable. | Provides a base layer of system-wide default settings. These settings have the lowest precedence and are intended to be overridden by user, project, or system override settings.                                         |
| User settings file    | `~/.qwen/settings.json` (where `~` is your home directory).                                                                                                                                                                                                                     | Applies to all Qwen Code sessions for the current user.                                                                                                                                                                   |
| Project settings file | `.qwen/settings.json` within your project's root directory.                                                                                                                                                                                                                     | Applies only when running Qwen Code from that specific project. Project settings override user settings.                                                                                                                  |
| System settings file  | Linux： `/etc/qwen-code/settings.json` <br>Windows: `C:\ProgramData\qwen-code\settings.json` <br>macOS: `/Library/Application Support/QwenCode/settings.json`<br>The path can be overridden using the `QWEN_CODE_SYSTEM_SETTINGS_PATH` environment variable.                    | Applies to all Qwen Code sessions on the system, for all users. System settings override user and project settings. May be useful for system administrators at enterprises to have controls over users' Qwen Code setups. |

> [!note]
>
> **Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`.

### The `.qwen` directory in your project

In addition to a project settings file, a project's `.qwen` directory can contain other project-specific files related to Qwen Code's operation, such as:

- [Custom sandbox profiles](../features/sandbox) (e.g. `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`).
- [Agent Skills](../features/skills) under `.qwen/skills/` (each Skill is a directory containing a `SKILL.md`).

### Configuration migration

Qwen Code automatically migrates legacy configuration settings to the new format. Old settings files are backed up before migration. The following settings have been renamed from negative (`disable*`) to positive (`enable*`) naming:

| Old Setting                              | New Setting                                 | Notes                              |
| ---------------------------------------- | ------------------------------------------- | ---------------------------------- |
| `disableAutoUpdate` + `disableUpdateNag` | `general.enableAutoUpdate`                  | Consolidated into a single setting |
| `disableLoadingPhrases`                  | `ui.accessibility.enableLoadingPhrases`     |                                    |
| `disableFuzzySearch`                     | `context.fileFiltering.enableFuzzySearch`   |                                    |
| `disableCacheControl`                    | `model.generationConfig.enableCacheControl` |                                    |

> [!note]
>
> **Boolean value inversion:** When migrating, boolean values are inverted (e.g., `disableAutoUpdate: true` becomes `enableAutoUpdate: false`).

#### Consolidation policy for `disableAutoUpdate` and `disableUpdateNag`

When both legacy settings are present with different values, the migration follows this policy: if **either** `disableAutoUpdate` **or** `disableUpdateNag` is `true`, then `enableAutoUpdate` becomes `false`:

| `disableAutoUpdate` | `disableUpdateNag` | Migrated `enableAutoUpdate` |
| ------------------- | ------------------ | --------------------------- |
| `false`             | `false`            | `true`                      |
| `false`             | `true`             | `false`                     |
| `true`              | `false`            | `false`                     |
| `true`              | `true`             | `false`                     |

### Available settings in `settings.json`

Settings are organized into categories. Most settings should be placed within their corresponding top-level category object in your `settings.json` file. A few top-level settings like `proxy` and `plansDirectory` remain direct root keys for compatibility.

#### general

| Setting                                    | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Default     |
| ------------------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `general.preferredEditor`                  | string  | The preferred editor to open files in.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `undefined` |
| `general.outputStyle`                      | string  | Name of the output style that shapes how responses are written: a built-in (`Concise`, `Proactive`, `Explanatory`, `Learning`) or the name of a [custom style](../features/output-styles#custom-styles) — its frontmatter `name`, defaulting to the file name without `.md` (case-insensitive). Leave unset, or set `default`, for the default style. `--output-style` overrides it for one run. Change it mid-session with `/output-style`, which also persists the choice; a hand edit to this file takes effect on the next start. Ignored in `--bare` and `--safe-mode`. See [Output Styles](../features/output-styles). | `undefined` |
| `general.vimMode`                          | boolean | Enable Vim keybindings.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `false`     |
| `general.enableAutoUpdate`                 | boolean | Enable automatic update checks and installations on startup.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `true`      |
| `general.batchAutoCollect`                 | boolean | Collect this project's `/batch-api` tasks in interactive sessions when their batch finishes (also at startup) and write the results. Polls the provider over HTTP; never calls the model and never retries failed items. Requires restart.                                                                                                                                                                                                                                                                                                                                                                                   | `true`      |
| `general.showSessionRecap`                 | boolean | Auto-show a one-line "where you left off" recap when returning to the terminal after being away. Off by default. Use `/recap` to trigger manually regardless of this setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                | `false`     |
| `general.sessionRecapAwayThresholdMinutes` | number  | Minutes the terminal must be blurred before an auto-recap fires on focus-in. Only used when `showSessionRecap` is enabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `5`         |
| `general.gitCoAuthor.commit`               | boolean | Add a Co-authored-by trailer to git commit messages AND attach a per-file AI-attribution git note (`refs/notes/ai-attribution`) for commits made through Qwen Code. Disabling skips both.                                                                                                                                                                                                                                                                                                                                                                                                                                    | `true`      |
| `general.gitCoAuthor.pr`                   | boolean | Append a Qwen Code attribution line to pull request descriptions when running `gh pr create`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `true`      |
| `general.defaultFileEncoding`              | enum    | Default encoding for new files. Use `"utf-8"` (default) for UTF-8 without BOM, or `"utf-8-bom"` for UTF-8 with BOM. Only change this if your project specifically requires BOM.                                                                                                                                                                                                                                                                                                                                                                                                                                              | `"utf-8"`   |
| `general.voice.enabled`                    | boolean | Enable voice dictation in the prompt input. Also toggleable with the `/voice` command. Requires a transcription model (`voiceModel`) to be configured.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `false`     |
| `general.voice.mode`                       | enum    | How push-to-talk behaves: `"hold"` to talk while the key is held, or `"tap"` to start and tap (or pause) to stop and submit.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `"hold"`    |
| `general.voice.language`                   | string  | Preferred spoken language for voice transcription (e.g. `"english"`, `"chinese"`). Leave empty to auto-detect.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `""`        |
| `general.voice.keytermsFile`               | string  | Path to a custom keyterms file (one term per line, `#` for comments) that biases voice transcription toward domain-specific terms. Relative paths resolve from the workspace root; defaults to `.qwen/voice-keyterms.txt` when present. Read only in trusted workspaces. Only applies to Qwen ASR models (`qwen3-asr-*`).                                                                                                                                                                                                                                                                                                    | `""`        |
| `general.voice.refineTranscript`           | boolean | Clean up voice transcripts with the fast model before inserting them — removes filler words and fixes recognition errors while preserving meaning. Falls back to the raw transcript on failure, and is skipped when no fast model is configured.                                                                                                                                                                                                                                                                                                                                                                             | `true`      |
| `general.cleanupPeriodDays`                | number  | Days to retain `~/.qwen/file-history/` session backups used by `/rewind` and session debug logs under the runtime `debug/` directory. Data older than this is removed by a background pass that runs at most once per day. `0` = minimum retention (~1 hour): keeps sessions touched in the last hour plus the currently active one. Changes take effect after restart.                                                                                                                                                                                                                                                      | `30`        |
| `general.language`                         | enum    | Language for the user interface. Use `"auto"` to detect from system settings, or a language code (e.g. `"zh-CN"`, `"fr"`). Custom codes can be added by placing JS locale files in `~/.qwen/locales/`. See [i18n](../features/language). Requires restart.                                                                                                                                                                                                                                                                                                                                                                   | `"auto"`    |
| `general.outputLanguage`                   | string  | Language for model output. Use `"auto"` to detect from system settings, or set a specific language. Requires restart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `"auto"`    |
| `general.terminalBell`                     | boolean | Play a terminal bell sound when a response completes or needs approval.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `true`      |
| `general.preventSystemSleep`               | boolean | Prevent the system from sleeping while Qwen Code is streaming a model response or executing tools. Idle prompt time and permission prompts do not inhibit sleep. Read once at startup, so changes take effect after restart.                                                                                                                                                                                                                                                                                                                                                                                                 | `true`      |
| `general.chatRecording`                    | boolean | Save chat history to disk. Disabling this also prevents `--continue` and `--resume` from working. Requires restart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `true`      |

#### output

| Setting                 | Type    | Description                                                                                                                                                                                                                                                                                                               | Default  | Possible Values                     |
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------- |
| `output.format`         | string  | The format of the CLI output. With `stream-json`, runs started with a prompt behave as non-interactive (headless), matching `--output-format stream-json`. Flags validated at argv parse time (`--include-partial-messages`, `--input-format stream-json`) still require the explicit `--output-format stream-json` flag. | `"text"` | `"text"`, `"json"`, `"stream-json"` |
| `output.showTimestamps` | boolean | Show an `[HH:MM:SS]` timestamp before each assistant response.                                                                                                                                                                                                                                                            | `false`  |                                     |

#### review

| Setting                     | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Default     |
| --------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `review.attribution`        | boolean | Append the attribution footer naming the model and CLI version (e.g. `_— qwen3-coder via Qwen Code /review (v0.21.2)_`) to review bodies and inline comments posted by `/review`. Disable to post reviews without visible AI attribution: the footer is omitted and posted comments and body lists lose their `**[Critical]**`/`**[Suggestion]**` markers. The posts stay identifiable in the raw source: each carries an invisible severity marker (``) and the review body carries a ledger marker (``) — anything reading comment bodies (GitHub API automation, the workflows this setting couples to) still recognizes a `/review` artifact, and presubmit duplicate detection recognizes the reviewing account's earlier posts by the severity marker, though unattributed posts from other accounts escape it. Another consequence: qwen-autofix's Critical-only mode (engaged after round 5, or earlier when a counting window's diff-growth budget trips) no longer recognizes the posted findings as Critical and defers them. Disabling also withholds the model from the machine-ledger marker embedded in the review body, so in a fresh environment (CI, another clone — anywhere without a review cache) the incremental anchor recovered from the last posted review fails the same-model check and the re-review falls back to full-range. | `true`      |
| `review.effort`             | enum    | Default effort for `/review` when neither `--effort` nor a project-remembered explicitly typed level applies: `"low"`, `"medium"`, `"high"`, or `"auto"` (the built-in rule: high for PRs, medium for local changes). An explicit or remembered level wins; an effective `--comment` still forces high and `--fix` still floors at medium.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `"auto"`    |
| `review.comment`            | boolean | Treat every PR `/review` as if `--comment` was passed: findings are posted to the pull request without the flag. The post still binds to the PR named in the invocation. Enable only if you always want reviews published.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `false`     |
| `review.severityFloor`      | enum    | The lowest severity a PR `/review` posts when `--severity-floor` is not given: `"auto"` (the round-adaptive default — Suggestions post through round 5, only Criticals from round 6, with otherwise-postable high-confidence Suggestions — and a Critical classified fails-closed on new surface — recorded and deferred, and rounds 2–5 deferring new Suggestions on code unchanged since the previous round; low-confidence and Nice-to-have findings stay terminal-only), `"critical"` (that posture from round 1), or `"suggestion"` (Suggestions post at every round; turns the convergence posture off). Non-PR targets have no rounds and ignore this.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `"auto"`    |
| `review.reverseAuditRounds` | number  | Lower the reverse-audit loop's round cap for every high-effort review. The cap otherwise follows the diff topology (10 small / 5 chunked; a huge diff is 3 under an explicit review deadline — CI's `QWEN_REVIEW_DEADLINE_EPOCH` or `--deadline <minutes>` at capture — and 5 under the plan's default wall). This can only **lower** whichever tier applies: a value below 3, above the tier, or not a whole number above zero is ignored. Cutting the cap does not make reviews converge sooner — the loop ends on two consecutive dry rounds — it makes them stop before converging more often, and every such stop caps the verdict at Comment.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `0` (unset) |

These settings are read from operator scopes only (User, System, and SystemDefaults); values in a workspace `.qwen/settings.json` are ignored, so a repository cannot set review policy for its reviewers.

#### ui

| Setting                                 | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Default       |
| --------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `ui.theme`                              | string           | The color theme for the UI. See [Themes](../configuration/themes) for available options.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `"Qwen Dark"` |
| `ui.customThemes`                       | object           | Custom theme definitions.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `{}`          |
| `ui.brand.name`                         | string           | Product name the Web Shell presents in its sidebar, welcome header, About panel and browser tab title. Sanitized to a single line and capped at 80 characters. Read from operator scopes only (User, System, SystemDefaults) — a workspace `.qwen/settings.json` cannot rebrand the shell. A placeholder value that substitution would change (`$VAR`/`${VAR}` with the variable set) is refused with a warning on the daemon's stderr, because the substitution source is process-wide and a workspace could supply it; an unresolvable placeholder is kept verbatim, so a typo'd variable shows as literal text rather than silently falling back. Not editable from the in-browser Settings page. The terminal banner has its own separate setting, `ui.customBannerTitle`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `""`          |
| `ui.brand.logoPath`                     | string           | Path to an SVG used as the Web Shell sidebar logo and browser favicon. A leading `~` is expanded, and a relative path resolves against the directory of the settings file that declares it. Must be a regular file (not a symlink, and not reachable through more than one hard link) of at most 32 KiB — on disk and once UTF-8-decoded — whose root element is a namespaced `<svg>` (the `xmlns` attribute, or an `xmlns:svg` binding on a prefix-bound root, is what makes it renderable as an image). Environment variable placeholders that would resolve are refused, as with `ui.brand.name`. Read from operator scopes only. A rejected file logs a warning on the daemon's stderr and falls back to the built-in logo. A root `<svg>` with no usable `viewBox` or explicit positive width/height is accepted but logs an advisory, and so is a prefix-bound root whose unprefixed elements have no default-namespace binding, because the browser may render it blank at the sidebar's fixed size.                                                                                                                                                                                                                                                                                               | `""`          |
| `ui.allowedApprovalModes`               | string[]         | Execution approval modes offered by the Web Shell mode picker, `/approval-mode` dialog, custom footer and keyboard cycling. Omit to offer `default`, `auto-edit`, `auto` and `yolo`; an empty list offers none and disables the picker. A Web Shell host's `allowedApprovalModes` prop overrides this setting. User, System, and SystemDefaults scopes only; workspace values are ignored with a warning, because a cloned repository must not remove a restriction the operator set. Presentation only: it does not change the current mode or enforce backend permissions. Not shown in the Settings dialog; edit `settings.json` directly.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `undefined`   |
| `ui.statusLine`                         | object           | Custom status line configuration. Supports `command`, `refreshInterval`, `respectUserColors`, and `hideContextIndicator` options. See [Status Line](../features/status-line).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `undefined`   |
| `ui.hideWindowTitle`                    | boolean          | Hide the window title bar.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `false`       |
| `ui.hideTips`                           | boolean          | Hide all tips (startup and post-response) in the UI. See [Contextual Tips](../features/tips).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `false`       |
| `ui.hideBanner`                         | boolean          | Hide the startup ASCII logo and info panel. Tips and chat input still render unless `ui.hideTips` is also set.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `false`       |
| `ui.customBannerTitle`                  | string           | Replace the default `>_ Qwen Code` title in the banner info panel. The `(vX.Y.Z)` version suffix is always appended; auth, model, and path lines are not affected. Sanitized; capped at 80 characters.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `""`          |
| `ui.customBannerSubtitle`               | string           | Optional subtitle line rendered between the banner title and the auth/model line, in place of the blank spacer row. Sanitized; capped at 160 characters. Empty (default) keeps the original blank spacer.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `""`          |
| `ui.customAsciiArt`                     | string \| object | Replace the QWEN ASCII logo in the banner. Accepts an inline string (used for both width tiers), `{ "path": "./brand.txt" }` (relative paths resolve against the owning settings file's directory; read once at startup with `O_NOFOLLOW` on POSIX, capped at 64 KB), or `{ "small": ..., "large": ... }` for width-aware selection. Sanitized; capped at 200 lines × 200 columns per tier.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `undefined`   |
| `ui.showLineNumbers`                    | boolean          | Show line numbers in code blocks in the CLI output.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `true`        |
| `ui.renderMode`                         | string           | Default Markdown display mode. Use `"render"` for rich visual previews or `"raw"` to show source-oriented Markdown by default. Toggle during a session with `Alt/Option+M`; on macOS the terminal must send Option as Meta. See [Markdown Rendering](../features/markdown-rendering).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `"render"`    |
| `ui.showCitations`                      | boolean          | Show citations for generated text in the chat.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `false`       |
| `ui.history.collapseOnResume`           | boolean          | Whether to collapse history by default when resuming a session. Can be toggled via `/history collapse-on-resume` and `/history expand-on-resume`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `false`       |
| `ui.history.collapsePreviewCount`       | number           | Number of most recent user turns to keep visible when `ui.history.collapseOnResume` is enabled. `0` collapses all restored history by default; `-1` shows all restored history.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `0`           |
| `ui.compactMode`                        | boolean          | **RETIRED everywhere.** The CLI now always shows the compact, type-based tool view in the main transcript; press `Ctrl+O` to toggle expanded detail mode (expand or collapse all thinking blocks and tool outputs inline) instead of toggling a mode, and the web shell now fixes its compact view on as well. The key is kept only so existing settings files do not warn; writes are accepted but nothing reads the value.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `false`       |
| `ui.showToolCallDetails`                | boolean          | Show tool arguments and results inline. Set to `false` to render ordinary tool calls as a one-line summary. Click the row in Virtualized History or press `Ctrl+O` to expand details. Approval prompts, user-initiated shell commands, and focused interactive shells remain expanded.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `true`        |
| `ui.showToolCallArgs`                   | boolean          | Render tool calls on their own line with their full raw arguments inline, instead of the type-based compact summary that folds read/search/list batches into `Read 3 files`. Recovers parameters the per-tool description summarizes away (e.g. `Edit` normally shows only the filename). Useful when debugging MCP integrations or tool schemas. The args row is capped at 2 wrapped lines (and at most 1000 characters), so a batch of pending calls cannot outgrow the terminal; press `Ctrl+O` to lift the cap and expand result output too. Two cases keep the compact view: groups of running parallel subagents, which the live agent roster owns — expanding them re-inflates the live frame past the terminal height (#5798) — and daemon-attached sessions, which do not carry arguments across the daemon boundary. Use `Ctrl+O` there. TUI only — the web shell is unaffected.                                                                                                                                                                                                                                                                                                                                                                                                                | `false`       |
| `ui.shellOutputMaxLines`                | number           | Max number of shell output lines shown inline. Set to `0` to disable the cap and show full output. Hidden lines are surfaced via the `+N lines` indicator. Errors, `!`-prefix user-initiated commands, confirming tools, and focused embedded shells always show full output.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `5`           |
| `ui.enableWelcomeBack`                  | boolean          | Show welcome back dialog when returning to a project with conversation history. When enabled, Qwen Code will automatically detect if you're returning to a project with a previously generated project summary (`.qwen/PROJECT_SUMMARY.md`) and show a dialog allowing you to continue your previous conversation or start fresh. If you choose **Start new chat session**, that choice is remembered for the current project until the project summary changes. This feature integrates with the `/summary` command and quit confirmation dialog.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `true`        |
| `ui.accessibility.enableLoadingPhrases` | boolean          | Enable loading phrases (disable for accessibility).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `true`        |
| `ui.accessibility.screenReader`         | boolean          | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `false`       |
| `ui.customWittyPhrases`                 | array of strings | A list of custom phrases to display during loading states. When provided, the CLI will cycle through these phrases instead of the default ones.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `[]`          |
| `ui.showResponseTokensPerSecond`        | boolean          | Show a live tokens/sec estimate next to the response token counter while the model is streaming. This is a generation-speed hint, not an ETA or completion percentage. Takes effect in the next session.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `false`       |
| `ui.enableFollowupSuggestions`          | boolean          | Enable [followup suggestions](../features/followup-suggestions) that predict what you want to type next after the model responds. Suggestions appear as placeholder text and are accepted with Tab, Enter, or Right Arrow (which fill the input — they do not auto-submit). On by default; set to `false` to opt out.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `true`        |
| `ui.enableCacheSharing`                 | boolean          | Use cache-aware forked queries for suggestion generation. Reduces cost on providers that support prefix caching (experimental).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `true`        |
| `ui.enableSpeculation`                  | boolean          | Speculatively execute accepted suggestions before submission. Results appear instantly when you accept (experimental).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `false`       |
| `ui.showStatusInTitle`                  | boolean          | Show the Qwen Code session name and status in the terminal window title.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `true`        |
| `ui.disableWorkflowKeywordTrigger`      | boolean          | When `true`, disables the single-turn `workflow` and `ultracode` reminder for human submissions across terminal, IDE, Web and explicitly human-origin SDK messages. Plain headless and automated input do not trigger it. Requires workflows to be enabled. See [workflow intent](../workflow-intent.md).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `false`       |
| `ui.enableUserFeedback`                 | boolean          | Show an optional feedback dialog after conversations to help improve Qwen performance.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `true`        |
| `ui.compactInline`                      | boolean          | **REMOVED.** Retired together with `ui.compactMode` — compact view is now always on in both the TUI and the web shell. The old setting is silently ignored (no startup warning).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `N/A`         |
| `ui.useTerminalBuffer`                  | boolean          | Render conversation history in an in-app scrollable viewport instead of the terminal scrollback buffer. Enabled by default in compatible interactive terminals to avoid flicker, scroll-storm, and interface freeze on long sessions, after Ctrl+O, after Ctrl+E / Ctrl+F (expand), after window resize, or when alt-tabbing back. Screen reader mode and non-interactive output such as piped stdout or CI use append-only terminal output instead. Scroll with `Shift+↑/↓` (line), `PgUp`/`PgDn` (page), `Ctrl+Home/End` (top/bottom), or the mouse wheel. Also enables mouse interactions: click an option in a menu/dialog to select it, hover to highlight it, and click in the prompt to position the cursor. Does NOT use the host terminal scrollback while enabled. Drag to select text in the viewport (double/triple click selects a word/line), copied on release. To use the terminal’s own selection instead, hold Shift (or Option on macOS) while dragging. A single click opens an http(s) hyperlink under the pointer (other link schemes are copied to the clipboard), and right-click over a link or a text selection opens an in-app context menu. These mouse interactions are controlled by `ui.mouseTracking`; disable that setting to hand the mouse fully back to the terminal. | `true`        |
| `ui.showScrollbar`                      | boolean          | Show the auto-hiding scrollbar in the in-app scrollable viewport (Virtualized History). The bar appears while scrolling and fades out when idle. Disable to hide it entirely. Only applies in the interactive terminal UI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `true`        |
| `ui.mouseTracking`                      | boolean          | Enable in-app SGR mouse tracking. While enabled, Qwen Code captures mouse events for text selection, click-to-position in text inputs, row hover, history-item toggling, and viewport scrolling. Because the terminal forwards all mouse events to the app, Qwen Code supplies its own equivalents for what the terminal can no longer do natively: a single click opens an http(s) hyperlink under the pointer (other link schemes are copied to the clipboard), and right-click over a link or a text selection opens an in-app context menu with Open Link / Copy Link Address / Copy Selection. Disable to hand the mouse fully back to the terminal (native right-click menu and link clicks); this turns off all in-app mouse interaction, and in Virtualized History the wheel no longer scrolls the transcript — use Shift+↑/↓, PgUp/PgDn, or Ctrl+Home/End instead (pair with `ui.useTerminalBuffer: false` to restore native terminal scrollback). Only applies in the interactive terminal UI.                                                                                                                                                                                                                                                                                                 | `true`        |
| `ui.arrowKeysScrollTranscript`          | boolean          | Let a bare ↑/↓ scroll the transcript while the composer is empty in Virtualized History, instead of recalling input history. Off by default: ↑/↓ walk your previous prompts, and the transcript scrolls with Shift+↑/↓ (line), PgUp/PgDn (page), Ctrl+Home/End (top/bottom), or the mouse wheel. Turn it on only for a terminal or multiplexer that does not pass mouse reporting through and turns wheel/trackpad scrolling into ↑/↓ key presses, where that burst of ↑/↓ would otherwise silently replace the composer with an older prompt. While it is on, use Ctrl+P / Ctrl+N for input history from an empty composer. Only applies in the interactive terminal UI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `false`       |
| `ui.hideBuiltinWorktreeIndicator`       | boolean          | Hide the built-in `⎇ worktree-<branch> (<slug>)` line in the Footer. The worktree state is still passed to custom statusline scripts via the stdin payload. Keep at the default unless your custom statusline renders the worktree itself.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `false`       |

#### ide

| Setting            | Type    | Description                                          | Default |
| ------------------ | ------- | ---------------------------------------------------- | ------- |
| `ide.enabled`      | boolean | Enable IDE integration mode.                         | `false` |
| `ide.hasSeenNudge` | boolean | Whether the user has seen the IDE integration nudge. | `false` |

#### privacy

| Setting                          | Type    | Description                            | Default |
| -------------------------------- | ------- | -------------------------------------- | ------- |
| `privacy.usageStatisticsEnabled` | boolean | Enable collection of usage statistics. | `true`  |

#### model

| Setting                                            | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Default     |
| -------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `model.name`                                       | string  | The Qwen model to use for conversations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `undefined` |
| `model.reasoningEffort`                            | enum    | How hard reasoning-capable models think, applied across all providers. Set with the [`/effort`](../features/commands) command (`low`, `medium`, `high`, `xhigh`, `max`). Each provider maps and clamps this to what the active model supports (e.g. Gemini caps at `high`; Anthropic clamps tiers a model lacks). Leave unset to use the model/provider default.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `undefined` |
| `model.baseUrl`                                    | string  | Persisted automatically by the model picker to disambiguate when multiple `modelProviders` entries share the same model id. Not intended to be set by hand — use the `/model` picker or a `modelProviders` entry instead; a stale hand-edited value can silently route requests to a different same-id provider.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `undefined` |
| `model.sessionTokenLimit`                          | number  | Maximum recorded prompt token count allowed before sending the next message. `-1` means unlimited; `0` is also treated as unlimited (unlike `model.maxToolCalls`, where `0` disallows all calls). When the recorded prompt count exceeds the limit, the next send is dropped (the session is not aborted).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `-1`        |
| `model.maxSessionTurns`                            | integer | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `-1`        |
| `model.maxWallTimeSeconds`                         | number  | Wall-clock budget for headless / unattended runs, in seconds. `-1` means unlimited. Overridable per-invocation via `--max-wall-time`, which requires a positive duration (`90`, `30s`, `5m`, `1h`, `1.5h`); the minimum is 1 second — sub-second values (`500ms`, `0.5`) are rejected as typos. Omit the flag to fall back to this setting. Aborts with exit code 55 when exceeded.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `-1`        |
| `model.goalTokenBudget`                            | integer | Autonomous spend window armed on each new Goal, in tokens as counted by the Goal meter (`totalTokenCount` summed over the model calls the Goal makes in its own turns). A Goal that spends its window gets one wind-down turn to hand off, then stops until you resume it, which arms another window. Unset uses the built-in default of 30,000,000; `-1` means unlimited. Zero, values above 300,000,000 (10x the default, a typo guard), other negative, fractional, or non-number values are rejected at startup.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `30000000`  |
| `model.goalCheckpointTimeoutSeconds`               | integer | Deprecated. Accepted but no longer used: Goals no longer run evidence-checkpoint model calls, because the verifier reads the transcript directly (see [Goals](../features/goals.md)). The key is still accepted so that existing settings files keep loading; its value is ignored.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `180`       |
| `model.goalMaxTurns`                               | integer | Goal-turn window armed on each new Goal. Every finished Goal turn counts, including user-driven turns; user turns are still admitted at the ceiling, but they can make the next autonomous continuation a wind-down. A Goal that reaches the ceiling gets one wind-down turn to hand off, then stops as usage-limited until you resume it, which authorizes another window on top of the turns already finished. Unset runs Goals with no turn ceiling, and `-1` says so explicitly -- but the opt-out only takes a ceiling off a Goal that has already spent it, on the resume or edit that follows; a Goal still under its ceiling keeps it. A ceiling is armed only on a Goal created after the change, so bounding a Goal already on the record means replacing it with `/goal set`, which starts a new Goal at revision 1 with its meters reset and its earlier evidence no longer citable, or clearing it and starting again. Zero, values above 10,000, other negative, fractional, or non-number values are rejected at startup. Changes take effect after restart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | (none)      |
| `model.goalMaxActiveMinutes`                       | integer | Active-time window armed on each new Goal, in minutes of wall time while the Goal remains active, including waits and idle time between turns. Paused, blocked or stopped time does not count, nor does downtime across a restart; a suspended process is still charged. A Goal that reaches the ceiling gets one wind-down turn to hand off, then stops as usage-limited until you resume it, which authorizes another window measured from where it stopped. The ceiling is read between turns, not by a timer, so a Goal can run well past it before it stops. Active time is measured between recorded transitions, so time in a turn that a restart interrupted is not charged. Unset runs Goals with no time ceiling, and `-1` says so explicitly -- but the opt-out only takes a ceiling off a Goal that has already spent it, on the resume or edit that follows. A ceiling is armed only on a Goal created after the change, so bounding a Goal already on the record means replacing it with `/goal set`, which starts a new Goal at revision 1 with its meters reset and its earlier evidence no longer citable, or clearing it and starting again. Zero, values above 10,080 (one week), other negative, fractional, or non-number values are rejected at startup. Changes take effect after restart.                                                                                                                                                                                                                                                                                                                                                                            | (none)      |
| `model.maxToolCalls`                               | number  | Cumulative tool-call budget for a run (counts every executed tool, success or failure; `structured_output` under `--json-schema` is exempt). `-1` means unlimited; `0` means "no tool calls allowed". Capped at 1,000,000 to catch typos. Overridable via `--max-tool-calls`. Aborts with exit code 55 when exceeded.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `-1`        |
| `model.maxSubagentDepth`                           | number  | Maximum sub-agent nesting depth (1-based levels: a top-level sub-agent is level 1). `1` keeps sub-agents available but disables nesting — the pre-nesting behavior. Values clamp to the range 1–100; non-finite values fall back to the default. Teammates, forks, and workflow-spawned agents never nest regardless of this setting. Overridable via `--max-subagent-depth`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `5`         |
| `model.generationConfig`                           | object  | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, `retryInitialDelayMs`, `retryMaxDelayMs`, `enableCacheControl`, `enableRequestMetadata` (DashScope only; unset sends the request `metadata` tracing object for qwen-family models alone, `true` sends it for any model, `false` never sends it; for provider models, define it in `modelProviders[].generationConfig.enableRequestMetadata`, since this `model.generationConfig` copy is ignored when a matching provider entry exists, and the provider reads only that model's own value, never the session's, so a side model under a different provider is not decided by the main model's own configuration), `splitToolMedia` (default `true`; splits tool-returned media — including images read by the built-in read_file — into a follow-up user message instead of the spec-violating `role: "tool"` message, so strict OpenAI-compatible servers like doubao / new-api / LM Studio can see it; set `false` to restore the legacy embed-in-tool behavior), `toolResultContentFormat` (default `"parts"`; set `"string"` only for legacy OpenAI-compatible runtimes whose tool templates ignore text content parts), `contextWindowSize` (override model's context window size), `modalities` (override auto-detected input modalities), `customHeaders` (custom HTTP headers for API requests), and `extra_body` (additional body parameters for OpenAI-compatible and OpenAI Responses API requests), along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. | `undefined` |
| `model.chatCompression.contextPercentageThreshold` | number  | **REMOVED.** Replaced by `context.autoCompactThreshold` (see `#### context` section below). Auto-compaction now uses a three-tier threshold ladder (warn / auto / hard) computed internally from the model's context window via the `computeThresholds()` function. The old setting is silently ignored (no startup warning). See PR #4345 / `docs/design/auto-compaction-threshold-redesign.md` for the redesign rationale.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `N/A`       |
| `model.chatCompression.maxRecentFilesToRetain`     | number  | Number of most-recently-touched files whose current content is restored (embedded if small, otherwise referenced by path) into history after auto-compaction. `0` restores none. Env override: `QWEN_COMPACT_MAX_RECENT_FILES`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `5`         |
| `model.chatCompression.maxRecentImagesToRetain`    | number  | Number of most-recent images (tool screenshots / user pastes) restored into history after auto-compaction. `0` restores none. Env override: `QWEN_COMPACT_MAX_RECENT_IMAGES`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `3`         |
| `model.chatCompression.enableScreenshotTrigger`    | boolean | When `true`, auto-compaction also fires once the number of tool-returned images accumulated in history reaches `screenshotTriggerThreshold`, independent of token usage — aimed at computer-use sessions where frequent screenshots dilute model attention. Counts only images returned inside tool results, not user-pasted images. Env override: `QWEN_COMPACT_SCREENSHOT_TRIGGER` (`1`/`true`/`0`/`false`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `true`      |
| `model.chatCompression.screenshotTriggerThreshold` | number  | Tool-returned image count at or above which the screenshot trigger fires (only when `enableScreenshotTrigger`). Compaction resets the count — surviving images are re-embedded as top-level parts, which the trigger doesn't count — so it won't immediately re-fire. Env override: `QWEN_COMPACT_SCREENSHOT_THRESHOLD`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `20`        |
| `model.skipNextSpeakerCheck`                       | boolean | Skip the next speaker check.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `true`      |
| `model.skipLoopDetection`                          | boolean | Disables streaming loop detection checks. Defaults to `true` (loop detection is skipped) to avoid false positives interrupting legitimate workflows. Set to `false` to re-enable streaming loop detection — useful as a guardrail in headless / non-interactive runs where stuck repetition can otherwise waste budget. In daemon/ACP sessions, which run none of the other streaming detectors, re-enabling also activates a global-duplicate tool-call halt; the always-on per-turn tool-call cap and an invalid-tool-params stagnation guard run there regardless of this setting. Whether these guards remind or stop is set by `model.loopProtection`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `true`      |
| `model.loopProtection`                             | string  | What a loop detector does when it fires. `"warn"` (default) reminds the model once, appended to the next tool results, and does not stop the turn, except for content and thought repetition, which happens mid-stream where no reminder can be delivered; `"stop"` stops the turn where it always has; the always-on guards (identical calls, shell inspection, invalid tool parameters, and stuck repetition past the adaptive cap) first remind the model once, appended to the next tool results, when the turn reaches their reminder point before the stop, while the opt-in heuristics stop without a reminder; `"off"` neither reminds nor stops and only records a telemetry event. Budgets stop in every mode: an explicit `model.maxToolCallsPerTurn`, the 1000-call backstop of the adaptive default, and the duplicate provider tool-call id guard. Under `"warn"` and `"off"` a stuck turn therefore runs until a budget stops it. Applies to the CLI, subagents and daemon/ACP sessions. `model.skipLoopDetection` still decides whether the heuristic detectors run, and the ACP repeated-tool-failure guard keeps its own setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `"warn"`    |
| `model.maxToolCallsPerTurn`                        | integer | Per-turn tool-call cap (one model turn plus its tool-result continuations; blocking Stop-hook continuations and runtime-scheduled Goal turns each start a fresh budget). When set explicitly, this value is a hard cap: the turn halts on the next tool call after it is reached (the released behavior). When left unset (default 100), the cap is adaptive: once the turn exceeds 100 it acts only when the model repeats the same call (a stuck loop; see `model.loopProtection`); a productive turn (diverse calls) continues up to a hard backstop of 1000, which always halts. The adaptive default applies to the interactive TUI, non-interactive (`-p` / JSON / stream-JSON) core-client runs, and daemon/ACP sessions alike. Daemon/ACP sessions evaluate the cap once per tool batch, before execution: a batch that would cross an explicit cap or the hard backstop is skipped whole, so a turn never executes past either (it can halt up to one batch short), while the adaptive soft cap is exceeded by design, up to the backstop. They also have no in-session disable. Always-on circuit breaker against runaway turns, independent of `model.skipLoopDetection`. At this threshold a long continuous read-only phase also receives a one-shot convergence reminder (asking the model to consolidate gathered information or state a blocker); it grants no new permission, and only the first non-read tool or a new turn can make another one possible. Set to `0` or a negative value to disable the cap. Choosing "Disable loop detection for this session" in the loop-detected dialog also suppresses both the cap and the reminder for the rest of the session.    | `100`       |
| `model.skipStartupContext`                         | boolean | Skips sending the startup workspace context (environment summary and acknowledgement) at the beginning of each session. Enable this if you prefer to provide context manually or want to save tokens on startup.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `false`     |
| `model.enableOpenAILogging`                        | boolean | Enables logging of OpenAI API calls for debugging and analysis. When enabled, API requests and responses are logged to JSON files.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `false`     |
| `model.openAILoggingDir`                           | string  | Custom directory path for OpenAI API logs. If not specified, defaults to `logs/openai` in the current working directory. Supports absolute paths, relative paths (resolved from current working directory), and `~` expansion (home directory).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `undefined` |
| `model.openAILogRetentionDays`                     | number  | Days to retain OpenAI API log files written when `model.enableOpenAILogging` is on. Completed background housekeeping passes run at most once per day in interactive, headless, stream-json SDK, and ACP sessions. Short-lived non-interactive processes make best-effort progress, while persistent processes scan to completion. `0` = minimum retention (~1 hour). For a custom `model.openAILoggingDir`, configure retention at user or system scope; workspace-scoped retention is skipped because one custom directory can be shared by multiple workspaces. Changes take effect after restart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `7`         |

**Example model.generationConfig:**

```json
{
  "model": {
    "generationConfig": {
      "timeout": 60000,
      "streamIdleTimeoutMs": 300000,
      "contextWindowSize": 128000,
      "modalities": {
        "image": true
      },
      "enableCacheControl": true,
      "toolResultContentFormat": "parts",
      "customHeaders": {
        "X-Client-Request-ID": "req-123"
      },
      "extra_body": {
        "enable_thinking": true
      },
      "samplingParams": {
        "temperature": 0.2,
        "top_p": 0.8,
        "max_tokens": 1024
      }
    }
  }
}
```

**timeout (request timeout):**

`timeout` is the per-request timeout in milliseconds (default `120000`). Set it to `0` to disable the request timeout — matching the `QWEN_STREAM_IDLE_TIMEOUT_MS=0` convention — rather than aborting the request. It can also be set via the `QWEN_CODE_API_TIMEOUT_MS` environment variable. This is distinct from the two stream guards below.

**stream guards (OpenAI-compatible and Anthropic providers):**

Two guards bound a streaming response, each accepting `0` to disable. The Gemini generator does not implement them, which leaves the drip-fed shape below unbounded for Gemini models.

- `streamIdleTimeoutMs` (default `240000`) bounds inactivity _between_ streamed chunks: a stream that goes silent for this long is aborted as a retryable `ETIMEDOUT`. For provider-backed models, set it under the matching `modelProviders[providerId][].generationConfig`; for runtime models, use `model.generationConfig`. An explicit model value takes precedence over `QWEN_STREAM_IDLE_TIMEOUT_MS`, and `0` disables the idle guard.
- `QWEN_STREAM_MAX_LIFETIME_MS` (default `900000`) caps the _total_ upstream-wait time of one streaming response regardless of chunk flow — the bound a drip-fed stream that never completes cannot reset.

`streamMaxLifetimeMs` remains available only through `QWEN_STREAM_MAX_LIFETIME_MS` or, for embedders, `ContentGeneratorConfig.streamMaxLifetimeMs`; writing it into `settings.json` has no effect. The 15-minute lifetime cap still bounds a stream whose idle timeout you raise above it. Raise the lifetime environment variable likewise, or set it to `0`, if you rely on a longer window. Disabling `streamIdleTimeoutMs` alone does not disable this lifetime cap.

**max_tokens (output token limit):**

When neither `samplingParams.max_tokens` nor `QWEN_CODE_MAX_OUTPUT_TOKENS` is set, Qwen Code generally uses the selected model's declared output limit as the request's default output limit. If the response still hits that limit, Qwen Code may retry with an escalated limit (using a 64K floor) and then recover across continuation turns.

For OpenAI-compatible providers, `samplingParams` is also a wire-shape escape hatch: when it is set, its keys are passed through verbatim and Qwen Code does not synthesize a `max_tokens` default. Use this for provider-specific parameters such as `max_completion_tokens`.

To force a fixed output limit, set `samplingParams.max_tokens` in your settings or use the `QWEN_CODE_MAX_OUTPUT_TOKENS` environment variable. Explicit limits disable automatic output-token escalation.

**toolResultContentFormat:**

Controls how text-only tool results are serialized in OpenAI-compatible requests. The default `"parts"` keeps the standard content-part array shape. Set `"string"` only for legacy OpenAI-compatible runtimes whose tool templates ignore text content parts, such as older GLM-5.1 vLLM/SGLang templates. Tool-returned media is still controlled by `splitToolMedia`.

**contextWindowSize:**

Overrides the default context window size for the selected model. Qwen Code determines the context window using built-in defaults based on model name matching, with a constant fallback value. Use this setting when a provider's effective context limit differs from Qwen Code's default. This value defines the model's assumed maximum context capacity, not a per-request token limit.

When the selected model is defined in `modelProviders`, set
`contextWindowSize` in that provider entry's `generationConfig` instead of the
top-level `model.generationConfig`. Provider model entries are sealed, so
top-level generation settings do not fill missing provider fields.

**modalities:**

Overrides the auto-detected input modalities for the selected model. Qwen Code automatically detects supported modalities (image, PDF, audio, video) based on model name pattern matching. Use this setting when the auto-detection is incorrect — for example, to enable `pdf` for a model that supports it but isn't recognized. Format: `{ "image": true, "pdf": true, "audio": true, "video": true }`. Omit a key or set it to `false` for unsupported types.

**customHeaders:**

Allows you to add custom HTTP headers to all API requests. This is useful for request tracing, monitoring, API gateway routing, or when different models require different headers. For provider models, define `customHeaders` in `modelProviders[].generationConfig.customHeaders`. For runtime models without a matching provider entry, define it in `model.generationConfig.customHeaders`. No merging occurs between the two levels.

The `extra_body` field allows you to add custom parameters to the request body sent to the API. This is useful for provider-specific options that are not covered by the standard configuration fields. **Note: This field is supported for OpenAI-compatible providers (`openai`, `qwen-oauth`) and the OpenAI Responses API (`openai-responses`). It is ignored for Anthropic and Gemini providers.** On the `openai-responses` wire, the legacy `enable_thinking` key is translated into `reasoning.effort` rather than forwarded verbatim — use `reasoning.effort` directly for that provider instead. For provider models, define `extra_body` in `modelProviders[].generationConfig.extra_body`. For runtime models without a matching provider entry, define it in `model.generationConfig.extra_body`.

**enableRequestMetadata notes:**

- A side model (`fastModel`, compaction, title generation, a subagent on another model) that sets no `enableRequestMetadata` of its own falls back to the automatic qwen-family gate. It does not inherit the main model's explicit `true` or `false`, so a vendor-forwarded side model is never sent the tracing object because the main model asked for it. A `fastModel` id that is not listed in `modelProviders` is not used at all; those calls run on the main model and its configuration.
- Under Qwen OAuth the `model.generationConfig` block is not applied to requests, so this switch has no effect on that route.

**model.openAILoggingDir examples:**

- `"~/qwen-logs"` - Logs to `~/qwen-logs` directory
- `"./custom-logs"` - Logs to `./custom-logs` relative to current directory
- `"/tmp/openai-logs"` - Logs to absolute path `/tmp/openai-logs`

#### fastModel

| Setting     | Type   | Description                                                                                                                                                                                                                                                      | Default |
| ----------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `fastModel` | string | Model used for generating [prompt suggestions](../features/followup-suggestions) and speculative execution. Leave empty to use the main model. A smaller/faster model (e.g., `qwen3-coder-flash`) reduces latency and cost. Can also be set via `/model --fast`. | `""`    |

#### advisorModel

| Setting        | Type   | Description                                                                                                                                                                                                                                                                                                                             | Default |
| -------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `advisorModel` | string | Model used by [`/advisor`](../features/commands.md#17-second-opinion-advisor) for second-opinion reviews of the conversation. Leave empty to use the main model. A model at least as capable as the main model is recommended. Setting this sends the recent conversation transcript to that model, even when it uses another provider. | `""`    |

#### visionModel

| Setting       | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                         | Default |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `visionModel` | string | Image-capable model used as the vision bridge: when a text-only main model receives an image, or `read_file` needs the bounded PDF visual fallback, it is transcribed by this model first. Setting this explicitly authorizes bridge calls to that model even when it uses another provider; the tool display discloses the endpoint. Leave empty to auto-pick a same-provider vision model. Can also be set via `/model --vision`. | `""`    |

#### compactionModel

| Setting           | Type   | Description                                                                                                                                                                                                             | Default |
| ----------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `compactionModel` | string | Model used for chat compression (auto-compaction). Leave empty to fall back to the main model. A smaller or faster model can reduce compression latency and cost. Can also be set or cleared via `/model --compaction`. | `""`    |

#### imageModel

| Setting      | Type   | Description                                                                                                                                                                                                                                                                              | Default |
| ------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `imageModel` | string | Model used by the built-in `image_gen` tool. The selected route must set `supportsImageGeneration: true` (or legacy `imageOnly: true`) and declare an HTTPS `baseUrl` plus `envKey` in `modelProviders`. Leave empty to keep the tool unavailable. Can also be set via `/model --image`. | `""`    |

#### visionBridgeTimeoutMs

| Setting                 | Type    | Description                                                                                                                                                                                                                                                        | Default |
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `visionBridgeTimeoutMs` | integer | Per-attempt timeout in milliseconds for the vision bridge image transcription call (positive integer up to 2147483647; the bridge retries a timed-out attempt once with a fresh timeout). Unset uses the built-in 30s. Raise for slow or proxied vision endpoints. | unset   |

#### voiceModel

| Setting      | Type   | Description                                                                                                                                             | Default |
| ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `voiceModel` | string | Model used for voice transcription. Leave empty to keep voice dictation disabled until a voice model is selected. Can also be set via `/model --voice`. | `""`    |

#### modelFallbacks

| Setting          | Type   | Description                                                                                                                                                                                                                             | Default |
| ---------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `modelFallbacks` | string | Ordered list of fallback model IDs (comma-separated, max 3) to try when the primary model hits capacity errors (429/503/529). Example: `"qwen-plus,qwen-turbo"`. Can also be set via the `--fallback-model` CLI flag. Requires restart. | `""`    |

#### modelPricing

| Setting        | Type   | Description                                                                                                                                                        | Default     |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| `modelPricing` | object | Optional per-model pricing for cost estimation in `/stats model`. Example: `{ "qwen3-coder": { "inputPerMillionTokens": 0.30, "outputPerMillionTokens": 1.20 } }`. | `undefined` |

#### context

| Setting                                                     | Type                       | Description                                                                                                                                                                                                                                                                                                                                                                                                      | Default                          |
| ----------------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| `context.fileName`                                          | string or array of strings | The name of the context file(s).                                                                                                                                                                                                                                                                                                                                                                                 | `undefined`                      |
| `context.autoCompactThreshold`                              | number                     | Target fraction of the context window at which auto-compaction triggers. Must be greater than 0 and at most 1. Default is `0.85` (85%). Acts as a ceiling on the trigger: on large windows it is the effective trigger (~85%), while on smaller windows compaction may fire earlier to leave room to summarize. Replaces the old `model.chatCompression.contextPercentageThreshold`.                             | `undefined` (uses internal 0.85) |
| `context.importFormat`                                      | string                     | The format to use when importing memory.                                                                                                                                                                                                                                                                                                                                                                         | `undefined`                      |
| `context.includeDirectories`                                | array                      | Additional directories to include in the workspace context. Specifies an array of additional absolute or relative paths to include in the workspace context. Missing directories will be skipped with a warning by default. Paths can use `~` to refer to the user's home directory. This setting can be combined with the `--include-directories` command-line flag.                                            | `[]`                             |
| `context.loadFromIncludeDirectories`                        | boolean                    | Controls the behavior of the `/memory refresh` command. If set to `true`, `QWEN.md` files should be loaded from all directories that are added. If set to `false`, `QWEN.md` should only be loaded from the current directory.                                                                                                                                                                                   | `false`                          |
| `context.fileFiltering.respectGitIgnore`                    | boolean                    | Respect .gitignore files when searching.                                                                                                                                                                                                                                                                                                                                                                         | `true`                           |
| `context.fileFiltering.respectQwenIgnore`                   | boolean                    | Respect .qwenignore and configured custom ignore files when searching.                                                                                                                                                                                                                                                                                                                                           | `true`                           |
| `context.fileFiltering.customIgnoreFiles`                   | array                      | Project-root-relative ignore files to use instead of the default compatibility files (`.agentignore`, `.aiignore`) when `respectQwenIgnore` is enabled. `.qwenignore` is always included.                                                                                                                                                                                                                        | `[".agentignore", ".aiignore"]`  |
| `context.fileFiltering.enableRecursiveFileSearch`           | boolean                    | Whether to enable searching recursively for filenames under the current tree when completing `@` prefixes in the prompt.                                                                                                                                                                                                                                                                                         | `true`                           |
| `context.fileFiltering.enableFuzzySearch`                   | boolean                    | When `true`, enables fuzzy search capabilities when searching for files. Set to `false` to improve performance on projects with a large number of files.                                                                                                                                                                                                                                                         | `true`                           |
| `context.clearContextOnIdle.toolResultsThresholdMinutes`    | number                     | Minutes of inactivity before clearing old tool result content. Use `-1` to disable the idle trigger.                                                                                                                                                                                                                                                                                                             | `60`                             |
| `context.clearContextOnIdle.toolResultsNumToKeep`           | integer                    | Integer number of most-recent compactable tool results to preserve when clearing. Values below 1 are floored to 1.                                                                                                                                                                                                                                                                                               | `5`                              |
| `context.clearContextOnIdle.toolResultsTotalCharsThreshold` | number                     | Total compactable tool result output characters allowed in history before clearing oldest results. When exceeded, oldest results are cleared down to half this threshold (best effort) so later turns keep reusing the provider prompt cache instead of rewriting history every turn. Use `-1` to disable the size trigger. This is a soft threshold: protected recent tool results may keep the total above it. | `500000`                         |

#### Troubleshooting File Search Performance

If you are experiencing performance issues with file searching (e.g., with `@` completions), especially in projects with a very large number of files, here are a few things you can try in order of recommendation:

1. **Use an ignore file:** Create a `.qwenignore` or configured custom ignore file in your project root to exclude directories that contain a large number of files that you don't need to reference (e.g., build artifacts, logs, `node_modules`). Reducing the total number of files crawled is the most effective way to improve performance.
2. **Disable Fuzzy Search:** If ignoring files is not enough, you can disable fuzzy search by setting `enableFuzzySearch` to `false` in your `settings.json` file. This will use a simpler, non-fuzzy matching algorithm, which can be faster.
3. **Disable Recursive File Search:** As a last resort, you can disable recursive file search entirely by setting `enableRecursiveFileSearch` to `false`. This will be the fastest option as it avoids a recursive crawl of your project. However, it means you will need to type the full path to files when using `@` completions.

#### tools

Set `tools.codeModeOnly` to `true` to enable experimental Code Mode with lazy tool discovery. It defaults to `false` and requires a restart.

Set `tools.freeform` to `true` to send Code Mode's `exec` input as raw text on
OpenAI Responses models. It takes effect only when `tools.codeModeOnly` is also
`true` and the selected model uses `wireApi: "responses"`. Enable it only for
endpoints that support [Responses Custom Tools](https://developers.openai.com/api/docs/guides/function-calling#custom-tools). This setting is intentionally
hidden from the Settings dialog; edit it in the settings file. It defaults to
`false` and requires a restart.

The bridge-availability and missing-bridge warning rules below describe direct tool mode. CodeModeOnly discovers deferred descriptions and schemas through top-level `tool_search`, then invokes tools through `exec`. Search results do not change the tool declarations. It skips deferred preload and startup catalogs; `tools.eager` also reduces the initial `exec` description. When search is unavailable in the current scope, `exec` includes all allowed tool signatures. See [Lazy Code Mode](../../design/lazy-code-mode.md).

| Setting                              | Type              | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Default     | Notes                                                                                                                                                                                                                                                                                                                       |
| ------------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tools.freeform`                     | boolean           | Use raw text input for the Code Mode `exec` tool on OpenAI Responses models. Effective only when `tools.codeModeOnly` is `true` and the selected model uses `wireApi: "responses"`. Enable only for endpoints that support Responses Custom Tools.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `false`     | Requires restart: Yes                                                                                                                                                                                                                                                                                                       |
| `tools.sandbox`                      | boolean or string | Sandbox execution environment (can be a boolean or a path string).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.sandboxImage`                 | string            | Sandbox image URI used by Docker/Podman when `--sandbox-image` and `QWEN_SANDBOX_IMAGE` are not set.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.shell.enableInteractiveShell` | boolean           | Use `node-pty` for an interactive shell experience. When unset, explicit one-shot prompts use `child_process`; interactive TUI, ACP, stream-json input, stdin-only, and file-input sessions use PTY. Platform and availability fallbacks to `child_process` still apply.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | mode-based  |                                                                                                                                                                                                                                                                                                                             |
| `tools.shell.defaultTimeoutMs`       | number            | Default timeout, in milliseconds, for foreground shell commands started by the agent. A per-call timeout on the shell tool overrides this. When unset, foreground commands time out after 120000 ms (2 minutes). Set to 0 to disable the timeout.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.shell.heartbeatIntervalMs`    | number            | Interval, in milliseconds, between liveness heartbeats emitted while a foreground shell command produces no output. Heartbeats are forwarded to ACP clients and stream-json consumers so they can tell a silent command from a dead session. When unset, heartbeats fire every 10000 ms (10 seconds). Set to 0 to disable heartbeats.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.core`                         | array of strings  | **Deprecated.** Will be removed in next version. A non-empty list restricts the core tool set (file, shell, search and related built-ins) to an allowlist: core tools not in the list are disabled (fail-closed). Tools outside that set — dynamically discovered tools (MCP, skill) and synthetic/system built-ins such as `agent`, `list_agents`, plan-mode lifecycle tools, goal tools, `task_stop`, `send_message`, `tool_search` and `tool_call` — bypass the allowlist by design; use `permissions.deny` to block a tool's calls (for MCP tools it stays listed and is rejected at runtime), or `tools.disabled` / the per-server `excludeTools` filter to remove it from the registry outright. An empty list (`[]`) is treated as unset and disables nothing. `permissions.allow` cannot reproduce this restriction — it is pure auto-approval (#10075). Use `tools.eager` to restrict which eager-by-default tool schemas are sent initially (unlisted tools are deferred, not disabled — they stay reachable through `tool_search` + `tool_call` while both bridge tools are registered), and `permissions.deny` to block tools outright.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.exclude`                      | array of strings  | **Deprecated.** Use `permissions.deny` instead. Tool names to exclude from discovery. Not automatically migrated; the legacy setting remains honoured at startup.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.disabled`                     | array of strings  | Tool names hidden from the registry entirely. Unlike `permissions.deny` (which blocks calls at runtime), disabled tools are never registered, so they do not appear in `/tools` and cannot be discovered or called by the model. For example, `["enter_plan_mode"]` prevents the model from switching into plan mode on its own. Merged as a union across scopes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.visible`                      | array of strings  | Deferred tool names made visible at startup without requiring the ToolSearch + ToolCall bridge. Listed tools appear alongside core tools in the initial session. Merged as a union across scopes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.eager`                        | array of strings  | Allowlist of eager-by-default built-in tool names whose schemas remain eligible for the initial model request. Unlisted non-exempt tools are deferred instead: still registered, listed in `/tools`, and reachable through the `tool_search` + `tool_call` bridge. Tools already deferred by default stay on demand even when listed; use `tools.visible` to surface one at startup. `tool_search`, `tool_call`, `structured_output`, plan-mode lifecycle tools, `task_stop`, MCP tools, and `computer_use__*` tools are unaffected and keep their normal loading behaviour. An explicitly empty list (`[]`) is active and defers every non-exempt eager-by-default tool; omitting the setting means no restriction. Pairs with the ToolSearch + ToolCall bridge: when either half is not registered — `tools.toolSearch.enabled: false` (which denies both), a `tool_search` or `tool_call` deny rule, or a `tools.disabled` entry — the allowlist still withholds the schemas, but nothing can load them back, so the demoted tools that remain hidden are not offered to the model and cannot be reached through the bridge for that session (they stay registered and in `/tools`, a direct call by their own name is still evaluated and approved normally, and a warning is logged) — except tools also listed in `tools.visible`, which are declared upfront, and sessions whose live history contains a direct call to a still-hidden demoted tool, which any tool-set refresh (resume, MCP discovery, the first plan-mode entry in a session, a subagent definition change) re-declares. Use `permissions.deny` if you meant to remove them, or keep both bridge tools registered. Unusable entries (empty or malformed) are dropped with a warning and leave the rest of the list active. Later scopes replace earlier lists. Requires restart. | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.allowed`                      | array of strings  | **Deprecated.** Use `permissions.allow` instead. Tool names that bypass the confirmation dialog. Not automatically migrated; the legacy setting remains honoured at startup.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.approvalMode`                 | string            | Sets the default approval mode for tool usage.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `auto`      | Possible values: `plan` (analyze only, do not modify files or execute commands), `default` (require approval before file edits or shell commands run), `auto-edit` (automatically approve file edits), `auto` (LLM classifier auto-approves safe actions, blocks risky ones), `yolo` (automatically approve all tool calls) |
| `tools.discoveryCommand`             | string            | Command to run for tool discovery. When the `tools.eager` allowlist is active, a discovered tool not named in it is registered as deferred: it stays in `/tools` and is reachable through `tool_search` + `tool_call` while both bridge tools are registered, but its schema is not sent in the initial model request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.callCommand`                  | string            | Defines a custom shell command for calling a specific tool that was discovered using `tools.discoveryCommand`. The shell command must meet the following criteria: It must take function `name` (exactly as in [function declaration](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations)) as first command line argument. It must read function arguments as JSON on `stdin`, analogous to [`functionCall.args`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functioncall). It must return function output as JSON on `stdout`, analogous to [`functionResponse.response.content`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functionresponse).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `undefined` |                                                                                                                                                                                                                                                                                                                             |
| `tools.useRipgrep`                   | boolean           | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `true`      |                                                                                                                                                                                                                                                                                                                             |
| `tools.useBuiltinRipgrep`            | boolean           | Use the bundled ripgrep binary. When set to `false`, the system-level `rg` command will be used instead. This setting is only effective when `tools.useRipgrep` is `true`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `true`      |                                                                                                                                                                                                                                                                                                                             |
| `tools.workflowsEnabled`             | boolean           | Enable the Workflow tool, which lets the model author and run a script that orchestrates subagents in parallel. Off by default; a run can dispatch many subagents and spend tokens accordingly.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `false`     | User, System, and SystemDefaults scopes only; workspace values are ignored. Requires restart: Yes. Env overrides: `QWEN_CODE_ENABLE_WORKFLOWS=1` forces on; `QWEN_CODE_DISABLE_WORKFLOWS=1` forces off (disable wins).                                                                                                      |
| `tools.workflowSizeGuideline`        | enum              | Advisory size guideline for the dynamic workflows the model writes: `"small"` aims for fewer than 5 agents, `"medium"` fewer than 15, `"large"` fewer than 50, and `"unrestricted"` sends no guideline. It is not an enforced limit. It also sets the agent count at which a running workflow is flagged as a large workflow in the background-tasks view.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `"medium"`  | Possible values: `"small"`, `"medium"`, `"large"`, `"unrestricted"`. Requires restart: No; a change is announced to the model with your next message. Env override for the warning threshold: `QWEN_CODE_WORKFLOW_SIZE_WARNING_AGENTS`.                                                                                     |
| `tools.workflowNameOnly`             | boolean           | Restrict the model to running named workflows — saved workflows and the workflows extensions ship, called as `Workflow({ name, args })`. The model cannot run an inline `script` or a `scriptPath`, and a running script cannot nest `workflow({ scriptPath })`, so every run the model starts can be matched by a `Workflow(name:...)` permission rule. It does not replace an approval policy: the model can still save a new workflow file and run it by name, which a rule scoped to specific names or script digests asks about. Runs a host starts over ACP (`run-saved`, `run-script`, retry, rerun) are not restricted. In such a session the `/review` workflow fan-out is unavailable.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `false`     | Requires restart: Yes. Env: `QWEN_CODE_WORKFLOW_NAME_ONLY=1` turns it on too; a project `.env` cannot set it. A workspace may set this to `true` only; a workspace `false` is ignored.                                                                                                                                      |
| `goals.modelProposed`                | enum              | Controls the `propose_goal` tool, which lets the model propose a session Goal for you to approve: `alwaysAsk` shows every proposal in an approval dialog and nothing is set until you accept it; `"disabled"` removes the tool. A typed `/goal` is unaffected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `alwaysAsk` | User, System, and SystemDefaults scopes only; workspace values are ignored. Requires restart: Yes.                                                                                                                                                                                                                          |
| `tools.truncateToolOutputThreshold`  | number            | Truncate tool output if it is larger than this many characters. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `25000`     | Requires restart: Yes                                                                                                                                                                                                                                                                                                       |
| `tools.truncateToolOutputLines`      | number            | Maximum lines or entries kept when truncating tool output. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `1000`      | Requires restart: Yes                                                                                                                                                                                                                                                                                                       |
| `tools.toolSearch.enabled`           | boolean           | Review deferred tool schemas through ToolSearch and invoke them through the stable ToolCall bridge. Bridge review and invocation keep the tool list stable — the bridge never re-declares what it reveals — reducing prompt size without touching the prompt-cache prefix. The declaration list is not immutable, though: a session still re-declares on resume, whenever a tool-set refresh (MCP discovery, the first plan-mode entry in a session, a subagent definition change) finds a direct call to a still-hidden deferred tool in the live history, when a subagent definition change rewrites the agent tool's own description, and when an MCP server registers mid-session with `alwaysLoadTools: true`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `true`      | Requires restart: Yes                                                                                                                                                                                                                                                                                                       |
| `tools.toolSearch.threshold`         | number            | Context-window percentage used as the session-start budget for preloading ordinary deferred tools (bundled built-ins and MCP alike). Defaults to `0`, which performs no threshold-based preload; ordinary deferred tools normally stay behind the stable ToolSearch + ToolCall bridge, at the cost of one `tool_search` round trip before first use. Raise it to `N` so that, when every eligible deferred schema fits within `N`% of the context window, all are declared upfront for direct calls with no bridge round trip; otherwise they stay behind the bridge while both bridge tools are registered. Tools demoted by `tools.eager` are excluded from this preload and stay reachable on demand through that bridge while it is registered. Separate paths can still declare deferred tools at `0`: `tools.visible`; the live-history compatibility scan on every tool-set refresh (including resume, MCP discovery, first plan-mode entry, and subagent definition changes); the incomplete-bridge eager fallback; and daemon ACP's late registration, which explicitly reveals and pins `create_sub_session`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `0`         | Requires restart: Yes                                                                                                                                                                                                                                                                                                       |
| `tools.listDirectory.enabled`        | boolean           | Enable the built-in `list_directory` tool. Disabled by default because `glob` covers directory listing in most cases; the tool is also re-enabled automatically when explicitly listed in the `coreTools` allowlist (`--core-tools` / `tools.core`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `false`     | Requires restart: Yes                                                                                                                                                                                                                                                                                                       |
| `tools.todoWrite.enabled`            | boolean           | Enable the built-in `todo_write` tool and its system-prompt guidance. Disabled by default.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `false`     | Requires restart: Yes                                                                                                                                                                                                                                                                                                       |

> [!note]
>
> **Migrating from `tools.core` / `tools.exclude` / `tools.allowed`:** These legacy settings are **deprecated** but are not automatically migrated; they continue to work at startup. Migrate `tools.allowed` and `tools.exclude` manually to `permissions.allow` and `permissions.deny`. `tools.core` has no exact replacement; see the table below.

#### memory

| Setting                          | Type    | Description                                                                                                                                                                                                                                                                                                                                         | Default |
| -------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `memory.enableManagedAutoMemory` | boolean | Enable background extraction of memories from conversations.                                                                                                                                                                                                                                                                                        | `true`  |
| `memory.enableManagedAutoDream`  | boolean | Enable automatic consolidation (deduplication and cleanup) of collected memories.                                                                                                                                                                                                                                                                   | `true`  |
| `memory.enableAutoSkill`         | boolean | Enable background review for reusable project skills after tool-heavy sessions.                                                                                                                                                                                                                                                                     | `true`  |
| `memory.autoSkillConfirm`        | boolean | Ask for confirmation before auto-generated skills are added to the skill library. When off, auto-skills are saved immediately.                                                                                                                                                                                                                      | `true`  |
| `memory.enableTeamMemory`        | boolean | Enable a project memory tier shared with collaborators via the git-tracked `.qwen/team-memory/` directory. Writes to it are secret-scanned and reviewable in the git diff.                                                                                                                                                                          | `false` |
| `memory.enableTeamMemorySync`    | boolean | When team memory is enabled, automatically commit, fast-forward-pull, and push the `.qwen/team-memory/` directory at session start so collaborators stay in sync. Requires a configured git upstream.                                                                                                                                               | `false` |
| `memory.enableStructuredRecall`  | boolean | Switch memory recall from the flat `MEMORY.md` listing to the structured protocol (hierarchical memory tree, focused subtree, `search_memory` tool). While off, the background metadata migration is never scheduled, so the protocol costs no background model calls. Override with `QWEN_CODE_MEMORY_STRUCTURED_RECALL=0\|1`. Requires a restart. | `false` |
| `memory.agentTimeoutMinutes`     | number  | Max runtime in minutes for background memory agents (extraction, dream, remember, skill review, memory metadata migration). Unset uses each agent's built-in default (2–5 minutes); `0` disables the time limit. User/System/SystemDefaults scopes only; Workspace values are ignored with a warning.                                               | unset   |
| `memory.agentMaxTurns`           | number  | Max turns for background memory agents (extraction, dream, remember, skill review). Unset uses each agent's built-in default (5–8); `0` disables the turn limit. User/System/SystemDefaults scopes only; Workspace values are ignored with a warning.                                                                                               | unset   |

See [Memory](../features/memory) for details on how auto-memory works and how to use the `/memory`, `/remember`, and `/dream` commands.

For an external Mem0 service, configure `memory.mem0` in user or system settings. Its `envKey` references a credential variable (default `MEM0_API_KEY`), just like `modelProviders`; the top-level `env` field can supply the value without a shell export. `credentialEnv` remains a legacy alias; both fields must match if set together. See [Mem0](../features/mem0) for endpoint/protocol selection, credential-source precedence, default repository scope, and opt-in writes. No separate npm package, MCP registration, or manually configured write-confirmation Hook is needed.

#### agents

| Setting                        | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Default     |
| ------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `agents.builtin.exploreModel`  | string           | Model selector for the built-in Explore subagent. Use `inherit` for the main session model, `fast` for `fastModel`, a model ID, or an `authType:model-id` selector. A custom same-name Explore agent keeps its own model configuration. Requires restart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `inherit`   |
| `agents.modelGrades`           | object           | Maps semantic grade names exposed to the Agent tool to model selectors. Requires restart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `undefined` |
| `agents.allowedGrades`         | array of strings | Optional whitelist of configured model grades the Agent tool may use. Requires restart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `undefined` |
| `agents.crossSessionMessaging` | boolean          | Let Qwen Code sessions on this machine send each other messages over a per-session local socket. On by default: this session is discoverable by the others, takes peer messages under the review rules of `agents.crossSessionInbound`, and its model can address them from `send_message`. Two senders are delivered without review unless `agents.crossSessionInbound` is `hold` or `refuse`: processes this session starts, which inherit its child token, and a same-user process that claims this session's own review class, which nothing authenticates. Set to `false` to keep this session invisible and unreachable. Requires restart. Ignored in `--bare` and `--safe-mode`, which keep this session out of messaging whatever it says. A workspace may set this to `false` only; a workspace `true` is ignored, with a warning only when it would loosen an operator-set `false`. | `true`      |
| `agents.crossSessionInbound`   | enum             | What happens to inbound cross-session messages: `accept` delivers them, `hold` parks them for `/peers` review without letting the model act, and `refuse` opts this session out. Unset means [user-minted controllers](../features/commands.md#trusted-controllers) and this session's own child processes auto-deliver, while other sessions use [review-class parity](../features/commands.md#6-messaging-another-running-session); other messages are held for review. A workspace may only tighten this (`hold` or `refuse`, when stricter than the operator-set value or the unset default); an effective unrecognized value holds every message.                                                                                                                                                                                                                                        | `undefined` |

#### permissions

The permissions system provides fine-grained control over which tools can run, which require confirmation, and which are blocked.

**Decision priority (highest first): `deny` > `ask` > `allow` > _(default/interactive mode)_**

The first matching rule wins. Rules use the format `"ToolName"` or `"ToolName(specifier)"`.

| Setting             | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | Default     |
| ------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `permissions.allow` | array of strings | Rules for auto-approved tool calls (no confirmation needed). Merged across all scopes (user + project + system). This key is PURE auto-approval: it never removes, demotes, or hides a tool, and every built-in stays registered regardless of which tools the rules cover; tools deferred by default (such as `task_stop` or `monitor`) keep their usual on-demand loading via the `tool_search` + `tool_call` bridge (#10075). To keep a tool's schema out of the initial model request, use `tools.eager`; to block a tool outright, use a whole-tool `permissions.deny` rule — MCP tools are exempt from deny-based removal (see the `permissions.deny` row): hide them with the per-server `excludeTools` / `tools.disabled` filters. Exception: under the AUTO approval mode, dangerous allow rules are stashed rather than active, so a mid-session removal cannot touch the stash — when AUTO mode is exited the stashed rule is restored and auto-approves again until the session restarts. | `undefined` |
| `permissions.ask`   | array of strings | Rules for tool calls that always require user confirmation. Takes priority over `allow`. Like `allow`, this never affects whether a tool is registered.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `undefined` |
| `permissions.deny`  | array of strings | Rules for blocked tool calls. Highest priority — overrides both `allow` and `ask`. A whole-tool deny rule (no specifier) also removes the tool from the registry — for built-in tools and tools found via `tools.discoveryCommand`. MCP tools are exempt (their registration path only honours `disabledTools`): hide them with the per-server `excludeTools` / `tools.disabled` filters instead. Deny rules still block MCP tool calls at runtime.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `undefined` |

**Tool name aliases (any of these work in rules):**

| Alias                 | Canonical tool      | Notes                     |
| --------------------- | ------------------- | ------------------------- |
| `Bash`, `Shell`       | `run_shell_command` |                           |
| `Read`, `ReadFile`    | `read_file`         | Meta-category — see below |
| `Edit`, `EditFile`    | `edit`              | Meta-category — see below |
| `Write`, `WriteFile`  | `write_file`        |                           |
| `NotebookEdit`        | `notebook_edit`     |                           |
| `NotebookEditTool`    | `notebook_edit`     |                           |
| `Grep`, `SearchFiles` | `grep_search`       |                           |
| `Glob`, `FindFiles`   | `glob`              |                           |
| `ListFiles`           | `list_directory`    |                           |
| `WebFetch`            | `web_fetch`         |                           |
| `Agent`               | `task`              |                           |
| `Skill`               | `skill`             |                           |

**Meta-categories:**

Some rule names automatically cover multiple tools:

| Rule name | Tools covered                                        |
| --------- | ---------------------------------------------------- |
| `Read`    | `read_file`, `grep_search`, `glob`, `list_directory` |
| `Edit`    | `edit`, `write_file`, `notebook_edit`                |

> [!important]
> `Read(/path/**)` matches **all four** read tools (file read, grep, glob, and directory listing).
> To restrict only file reading, use `ReadFile(/path/**)` or `read_file(/path/**)`.

**Rule syntax examples:**

| Rule                          | Meaning                                                        |
| ----------------------------- | -------------------------------------------------------------- |
| `"Bash"`                      | All shell commands                                             |
| `"Bash(git *)"`               | Shell commands starting with `git` (word boundary: NOT `gitk`) |
| `"Bash(git push *)"`          | Shell commands like `git push origin main`                     |
| `"Bash(npm run *)"`           | Any `npm run` script                                           |
| `"Read"`                      | All file read operations (read, grep, glob, list)              |
| `"Read(./secrets/**)"`        | Read any file under `./secrets/` recursively                   |
| `"Edit(/src/**/*.ts)"`        | Edit TypeScript files under project root `/src/`               |
| `"WebFetch(api.example.com)"` | Fetch from `api.example.com` and all its subdomains            |
| `"mcp__puppeteer"`            | All tools from the puppeteer MCP server                        |

**Path pattern prefixes:**

| Prefix | Meaning                               | Example             |
| ------ | ------------------------------------- | ------------------- |
| `//`   | Absolute path from filesystem root    | `//etc/passwd`      |
| `~/`   | Relative to home directory            | `~/Documents/*.pdf` |
| `/`    | Relative to project root              | `/src/**/*.ts`      |
| `./`   | Relative to current working directory | `./secrets/**`      |
| (none) | Same as `./`                          | `secrets/**`        |

**Shell command bypass prevention:**

Permission rules for `Read`, `Edit`, and `WebFetch` are also enforced when the agent runs equivalent shell commands. For example, if `Read(./.env)` is in `deny`, the agent cannot bypass it via `cat .env` in a shell command. Supported shell commands include `cat`, `grep`, `curl`, `wget`, `cp`, `mv`, `rm`, `chmod`, and many more. Unknown/safe commands (e.g. `git`) are unaffected by file/network rules.

**Migrating from legacy settings:**

| Legacy setting  | Equivalent `permissions` rule        | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| --------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tools.allowed` | `permissions.allow`                  | Not automatically migrated; still honoured at startup                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `tools.exclude` | `permissions.deny`                   | Not automatically migrated; still honoured at startup                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `tools.core`    | `tools.eager` (+ `permissions.deny`) | Not auto-migrated to `permissions.allow`, which is pure auto-approval and cannot reproduce the allowlist restriction (#10075). `tools.eager` defers unlisted eager-by-default tools (they stay loadable via `tool_search`); `permissions.deny` removes built-ins from the registry entirely (MCP tools stay listed and are rejected at runtime — use `tools.disabled` / the per-server `excludeTools` filter to remove them outright). Neither preserves a non-empty `tools.core` allowlist's fail-closed guarantee over the core tool set: a built-in added in a future release registers until explicitly denied, so a deny list must be re-audited per release. An empty `tools.core` list is treated as unset and disables nothing. |

**Example configuration:**

```json
{
  "permissions": {
    "allow": ["Bash(git *)", "Bash(npm run *)", "Read(//Users/alice/code/**)"],
    "ask": ["Bash(git push *)", "Edit"],
    "deny": ["Bash(rm -rf *)", "Read(.env)", "WebFetch(malicious.com)"]
  }
}
```

> [!tip]
> Use `/permissions` in the interactive CLI to view, add, and remove rules without editing `settings.json` directly.

#### slashCommands

Controls which slash commands are available in the CLI. Useful for locking down
the command surface in multi-tenant or enterprise deployments.

| Setting                  | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Default     |
| ------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `slashCommands.disabled` | array of strings | Slash command names to hide and refuse to execute. Matched case-insensitively against the final command name (for extension commands this is the disambiguated form, e.g. `myext.deploy`), except that a [Skill](../features/skills) command is gated under **either** spelling — its registered name (`rust:pdf`) or the name its `SKILL.md` authors (`pdf`) — so an entry written before that prefix existed still gates it. **Merged as a union across scopes**, so workspace settings can add to but not remove entries defined in user or system settings. | `undefined` |

The same denylist can also be provided via the `--disabled-slash-commands` CLI
flag (comma-separated or repeated) and the `QWEN_DISABLED_SLASH_COMMANDS`
environment variable; values from all three sources are unioned together.

**Example — lock down built-ins for a sandboxed deployment:**

```json
{
  "slashCommands": {
    "disabled": ["auth", "mcp", "extensions", "ide", "quit"]
  }
}
```

With these values in a system-level `settings.json` (`/etc/qwen-code/settings.json`
or `QWEN_CODE_SYSTEM_SETTINGS_PATH`), users cannot shrink the denylist from
their own scope, and the disabled commands will not appear in autocomplete or
execute when typed.

> [!note]
> This setting only gates slash commands (e.g. `/auth`, `/mcp`). It does not
> affect tool permissions — see `permissions.deny` for that. It also does not
> intercept keyboard shortcuts such as `Ctrl+C` or `Esc`.

#### skills

Controls which [Skills](../features/skills) are exposed to the model.

| Setting                  | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Default     |
| ------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| `skills.disabledLevels`  | array of strings | Skill discovery levels to skip entirely. Supported values are `project`, `user`, `extension`, and `bundled`. Merged as a union across settings scopes. Use `["bundled"]` to hide every bundled skill while retaining host-provided skills. Note: `skills.directories` entries are discovered at the `user` level, so `["user"]` hides those too.                                                                                                                                                                                                                                                                                                                                                                                           | `undefined` |
| `skills.disabled`        | array of strings | Hard-disabled skill names. Matched case-insensitively and **merged as a union** across settings scopes, so project settings cannot override a user or system entry. Hidden skills do not appear in `<available_skills>` or as `/<name>` slash commands. An extension skill is matched under either spelling — its registered name (`rust:pdf`) or the name its `SKILL.md` authors (`pdf`) — so an entry written before the prefix existed keeps biting.                                                                                                                                                                                                                                                                                    | `undefined` |
| `skills.defaultDisabled` | array of strings | Skill names that start disabled but can be opted into through `skills.enabled`. Matched case-insensitively and merged as a union across settings scopes. An extension skill is matched under either spelling, as in `skills.disabled`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `undefined` |
| `skills.enabled`         | array of strings | Explicit opt-ins. Override a matching `skills.defaultDisabled` entry and, for an extension skill, the owning extension's own default and this workspace's stored enablement for it. Matched case-insensitively and merged as a union across settings scopes — against the skill's registered name **only**, so an extension skill needs `rust:pdf`: a bare `pdf` never matches as a grant — it only cancels an identically-spelled `skills.defaultDisabled` entry (per the identical-spelling rule below), and once cancelled the enablement stored for this workspace decides, else the owning extension's own default. This setting cannot override `skills.disabled` or re-enable skills from a `skills.disabledLevels`-excluded level. | `undefined` |
| `skills.directories`     | array of strings | Additional directories to scan for skills. Use absolute or `~`-prefixed paths; a relative path resolves against the working directory, not the project root. Each directory is scanned one level deep for `<name>/SKILL.md`. Skills found here load at the `user` level after `~/.qwen/skills/` and `~/.agents/skills/`, so a same-named skill in those wins. Merged as a union across settings scopes. Requires a restart and is ignored in bare and safe mode. Not gated by folder trust; see the note below.                                                                                                                                                                                                                            | `undefined` |

The precedence is `skills.disabled` > `skills.enabled` > `skills.defaultDisabled`. For example, a user can put a skill in `defaultDisabled` and a project can add the same name to `enabled`; a hard `disabled` entry at any scope still wins.

Every list holds literal skill names, matched case-insensitively after trimming, with no glob support. `skills.enabled` cancels a `skills.defaultDisabled` entry only when the two entries are spelled the same, because that step compares the entries themselves rather than resolving them to a skill — so an extension skill that must be both un-defaulted and opted in is written `rust:pdf` in both lists. See [Extension Skills](../features/skills#extension-skills) for why the two lists above accept either spelling and this one does not.

`skills.directories` lets you reuse a skills folder you already keep for another agent instead of copying it into `~/.qwen/skills/`:

```json
{
  "skills": {
    "directories": ["~/.claude/skills"]
  }
}
```

Skills from `skills.directories` load as personal Skills, so the folder-trust gate that covers project Skills does not apply to them: their `allowedTools` become session allow rules, their hooks run, and revoking folder trust suspends neither. Because the setting is merged across scopes, a trusted project's `.qwen/settings.json` can add entries too. List only directories you control, and check a repository's `skills.directories` before you trust its folder.

See [Share Skills with other tools](../features/skills#share-skills-with-other-tools).

#### mcp

| Setting                 | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                 | Default     |
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `mcp.serverCommand`     | string           | Command to start an MCP server.                                                                                                                                                                                                                                                                                                                                                                                                             | `undefined` |
| `mcp.allowed`           | array of strings | An allowlist of MCP servers to allow. Allows you to specify a list of MCP server names that should be made available to the model. This can be used to restrict the set of MCP servers to connect to. Supports glob patterns (`*` matches any sequence, `?` matches a single character — e.g. `"*puppeteer*"`); entries without glob characters are matched exactly. Note that this will be ignored if `--allowed-mcp-server-names` is set. | `undefined` |
| `mcp.excluded`          | array of strings | A denylist of MCP servers to exclude. A server listed in both `mcp.excluded` and `mcp.allowed` is excluded. Supports glob patterns (`*`, `?`) the same way as `mcp.allowed`. Note that this will be ignored if `--allowed-mcp-server-names` is set.                                                                                                                                                                                         | `undefined` |
| `mcp.toolIdleTimeoutMs` | number           | Idle timeout in milliseconds for MCP tool calls. If the MCP server does not produce any response or progress update within this time, the call is aborted. Must be between `10000` and `3600000`. Can be overridden via the `QWEN_CODE_MCP_TOOL_IDLE_TIMEOUT_MS` environment variable.                                                                                                                                                      | `300000`    |

> [!note]
>
> **Security Note for MCP servers:** These settings use simple string matching on MCP server names, which can be modified. If you're a system administrator looking to prevent users from bypassing this, consider configuring the `mcpServers` at the system settings level such that the user will not be able to configure any MCP servers of their own. This should not be used as an airtight security mechanism.

#### lsp

> [!warning]
> **Experimental Feature**: LSP support is currently experimental and disabled by default. Enable it using the `--experimental-lsp` command line flag.

Language Server Protocol (LSP) provides code intelligence features like go-to-definition, find references, and diagnostics.

LSP server configuration is done through `.lsp.json` files in your project root directory, not through `settings.json`. See the [LSP documentation](../features/lsp) for configuration details and examples.

#### security

| Setting                                 | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Default     |
| --------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `security.folderTrust.enabled`          | boolean          | Setting to track whether Folder trust is enabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `false`     |
| `security.auth.selectedType`            | string           | The currently selected authentication type.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `undefined` |
| `security.auth.enforcedType`            | string           | The required auth type (useful for enterprises).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `undefined` |
| `security.auth.useExternal`             | boolean          | Whether to use an external authentication flow.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `undefined` |
| `security.auth.apiKey`                  | string           | **Deprecated.** API key for OpenAI-compatible authentication. Migrate to `modelProviders` with `envKey` instead — see [Model Providers](./model-providers).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `undefined` |
| `security.auth.baseUrl`                 | string           | **Deprecated.** Base URL for the OpenAI-compatible API. Migrate to `modelProviders` instead — see [Model Providers](./model-providers).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `undefined` |
| `security.allowedInsecureVoiceBaseUrls` | array of strings | Complete voice provider base URLs that may use HTTP or resolve to private-network addresses. Each entry must include an explicit `http://` or `https://` scheme and the full path (for example, `/v1`); only URL serialization and trailing slashes are normalized. Wildcards are not supported; metadata, link-local, local-use NAT64, 6to4, and Teredo addresses remain blocked even when listed, as do hostnames that resolve to loopback; IPv4-mapped, IPv4-compatible, and well-known NAT64 (`64:ff9b::/96`) literals are classified by their embedded IPv4 address. Only User, System, and SystemDefaults scopes are honored. Use only for trusted endpoints in managed private networks. Cleartext HTTP also exposes the provider API key transmitted in the Authorization header. An allowlisted hostname is only as trustworthy as its DNS; prefer IP-literal entries when the gateway address is stable. The exact match covers the batch request URL; streaming transports connect to a WebSocket URL derived from it (same scheme, host, and port, `/api-ws/v1/...` path), not to the allowlisted path itself. | `[]`        |

#### serve

Persistent sub-session concurrency settings for [`qwen serve`](../qwen-serve). Changes require restarting the daemon. Non-positive or non-integer concurrency limits produce a warning and fall back to their built-in defaults.

| Setting                                   | Type    | Description                                                                                                                                                                            | Default |
| ----------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `serve.maxConcurrentSubSessionsPerCaller` | integer | Maximum number of in-flight sub-sessions that one caller session can create through `create_sub_session`. Must be at least `1`.                                                        | `16`    |
| `serve.maxConcurrentSubSessionsTotal`     | integer | Maximum number of in-flight sub-sessions across all callers in one workspace. Must be an integer from `1` through `1024`. Values above `1024` are clamped to `1024` without a warning. | `24`    |

#### advanced

| Setting                        | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                         | Default                  |
| ------------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `advanced.autoConfigureMemory` | boolean          | Automatically configure Node.js memory limits.                                                                                                                                                                                                                                                                                                                                                                                      | `false`                  |
| `advanced.dnsResolutionOrder`  | string           | The DNS resolution order.                                                                                                                                                                                                                                                                                                                                                                                                           | `undefined`              |
| `advanced.excludedEnvVars`     | array of strings | Environment variables to exclude from project context. Specifies environment variables that should be excluded from being loaded from project `.env` files. This prevents project-specific environment variables (like `DEBUG=true`) from interfering with the CLI behavior. Variables from `.qwen/.env` files are never excluded by this list; loader-affecting variables are always rejected from every `.env` scope (see below). | `["DEBUG","DEBUG_MODE"]` |
| `advanced.bugCommand`          | object           | Configuration for the bug report command. Overrides the default URL for the `/bug` command. Properties: `urlTemplate` (string): A URL that can contain `{title}` and `{info}` placeholders. Example: `"bugCommand": { "urlTemplate": "https://bug.example.com/new?title={title}&info={info}" }`                                                                                                                                     | `undefined`              |
| `plansDirectory`               | string           | Custom directory for approved Plan Mode files. Relative paths are resolved from the project root, and the resolved path must stay within the project root. If unset, plan files are stored in `~/.qwen/plans`. **Requires restart.** If the directory is inside the project root, add it to `.gitignore` to avoid committing plan files.                                                                                            | `undefined`              |

#### experimental

> [!warning]
>
> **Experimental features.** These toggles gate in-development capabilities and may change or be removed in future releases.

| Setting                                | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                      | Default |
| -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `experimental.mods`                    | boolean | Load experimental [Mods](../extension/mods) from enabled extensions in trusted local interactive terminal and headless sessions. Available as **Mods (Experimental)** in `/settings`. Requires restart. Does not install or enable extensions.                                                                                                                                                                                                   | `false` |
| `experimental.sessionWorkflow`         | boolean | Show the daemon Web Shell Session Workflow DAG and present the existing `plan` approval mode as **Plan & Review**. This changes presentation only: it does not add an approval mode, alter Todo execution behavior, or schedule dependencies. Changes take effect without restarting.                                                                                                                                                            | `false` |
| `experimental.cron`                    | boolean | Enable in-session cron/loop tools (`cron_create`, `cron_list`, `cron_delete`) so the model can create recurring prompts. Can be disabled via the `QWEN_CODE_DISABLE_CRON=1` environment variable. Requires restart.                                                                                                                                                                                                                              | `true`  |
| `experimental.todoStopGuard`           | boolean | Allow daemon and ACP sessions to continue after a natural model stop when the current work chain successfully wrote an unfinished top-level Todo list. Requires `tools.todoWrite.enabled`. Adds at most two consecutive primary-model calls without new user input; mid-turn user input starts a fresh two-attempt stage. It is not restored after process restart and is forced off in safe, bare, and Approval `plan` modes. Requires restart. | `false` |
| `experimental.sessionWriterLease`      | boolean | Enable cross-process write fencing for persisted ACP and daemon sessions. The value is frozen when the ACP or daemon process starts. All concurrent ACP writers must enable the setting; mixed versions or configurations remain unsafe. Interactive and headless recorders are unaffected. Requires process restart.                                                                                                                            | `false` |
| `experimental.cronRecurringMaxAgeDays` | number  | Days a recurring cron/loop job lives before auto-expiring (it fires one final time, then is deleted). Set to `0` to disable expiry so jobs run until deleted — useful for long-running daemon deployments. Can be overridden via the `QWEN_CODE_CRON_MAX_AGE_DAYS` environment variable. Requires restart.                                                                                                                                       | `7`     |
| `experimental.agentTeam`               | boolean | Enable agent-team collaboration tools (`team_create`, `task_create`, `task_update`, `send_message`, etc.) for multi-agent coordination. Can also be enabled via `QWEN_CODE_ENABLE_AGENT_TEAM=1`. Requires restart.                                                                                                                                                                                                                               | `false` |
| `experimental.artifact`                | boolean | Enable artifact tools. Enabled by default. In interactive, non-SDK sessions, the model can publish a self-contained HTML page as an interactive Artifact and open it in the browser. Non-SDK daemon sessions can use metadata-only `record_artifact`. Set this to `false` or use `QWEN_CODE_DISABLE_ARTIFACT=1` to disable both. Requires restart.                                                                                               | `true`  |
| `experimental.emitToolUseSummaries`    | boolean | Generate a short LLM-based label after each tool-call batch completes. See [Tool-Use Summaries](../features/tool-use-summaries). Requires a fast model to be configured (`fastModel`); silently skipped otherwise. Can be overridden per-session with `QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0` or `=1`.                                                                                                                                             | `true`  |

#### mcpServers

Configures connections to one or more Model-Context Protocol (MCP) servers for discovering and using custom tools. Qwen Code attempts to connect to each configured MCP server to discover available tools. If multiple MCP servers expose a tool with the same name, the tool names will be prefixed with the server alias you defined in the configuration (e.g., `serverAlias__actualToolName`) to avoid conflicts. Note that the system might strip certain schema properties from MCP tool definitions for compatibility. At least one of `command`, `url`, or `httpUrl` must be provided. If multiple are specified, the order of precedence is `httpUrl`, then `url`, then `command`.

| Property                                | Type             | Description                                                                                                                                                                                                                                                        | Optional |
| --------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| `mcpServers.<SERVER_NAME>.command`      | string           | The command to execute to start the MCP server via standard I/O.                                                                                                                                                                                                   | Yes      |
| `mcpServers.<SERVER_NAME>.args`         | array of strings | Arguments to pass to the command.                                                                                                                                                                                                                                  | Yes      |
| `mcpServers.<SERVER_NAME>.env`          | object           | Environment variables to set for the server process.                                                                                                                                                                                                               | Yes      |
| `mcpServers.<SERVER_NAME>.cwd`          | string           | The working directory in which to start the server.                                                                                                                                                                                                                | Yes      |
| `mcpServers.<SERVER_NAME>.url`          | string           | The URL of an MCP server that uses Server-Sent Events (SSE) for communication.                                                                                                                                                                                     | Yes      |
| `mcpServers.<SERVER_NAME>.httpUrl`      | string           | The URL of an MCP server that uses streamable HTTP for communication.                                                                                                                                                                                              | Yes      |
| `mcpServers.<SERVER_NAME>.headers`      | object           | A map of HTTP headers to send with requests to `url` or `httpUrl`.                                                                                                                                                                                                 | Yes      |
| `mcpServers.<SERVER_NAME>.timeout`      | number           | Timeout in milliseconds for requests to this MCP server.                                                                                                                                                                                                           | Yes      |
| `mcpServers.<SERVER_NAME>.trust`        | boolean          | Trust this server and bypass its tool call confirmations in a trusted workspace.                                                                                                                                                                                   | Yes      |
| `mcpServers.<SERVER_NAME>.description`  | string           | A brief description of the server, which may be used for display purposes.                                                                                                                                                                                         | Yes      |
| `mcpServers.<SERVER_NAME>.includeTools` | array of strings | List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default.                                        | Yes      |
| `mcpServers.<SERVER_NAME>.excludeTools` | array of strings | List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded. | Yes      |

#### telemetry

Configures logging and metrics collection for Qwen Code. For more information, see [telemetry](../../developers/development/telemetry.md).

| Setting                                     | Type    | Description                                                                                                                                                                                                                                                                              | Default   |
| ------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| `telemetry.enabled`                         | boolean | Whether or not telemetry is enabled.                                                                                                                                                                                                                                                     |           |
| `telemetry.target`                          | string  | Informational label for the telemetry destination (`local` or `gcp`). Does not control exporter routing; set `telemetry.otlpEndpoint` or `telemetry.outfile` to configure where data is sent.                                                                                            |           |
| `telemetry.otlpEndpoint`                    | string  | The endpoint for the OTLP Exporter.                                                                                                                                                                                                                                                      |           |
| `telemetry.otlpProtocol`                    | string  | The OTLP exporter protocol: `grpc` (gRPC/protobuf) or `http` (HTTP/JSON, `application/json`).                                                                                                                                                                                            |           |
| `telemetry.logPrompts`                      | boolean | Whether or not to include user prompt content and API request/response text in the logs.                                                                                                                                                                                                 |           |
| `telemetry.userId`                          | string  | Stable end-user identifier written to GenAI spans as the ARMS extension `gen_ai.user.id`. Prefer a pseudonymous value. Do not set a process-wide value for a shared multi-user daemon or channel instance.                                                                               |           |
| `telemetry.includeSensitiveSpanAttributes`  | boolean | When enabled, attaches verbatim user prompts, system prompts, tool inputs/outputs, and model responses to native OTel span attributes (in addition to log-to-span bridge spans). ⚠️ Streams sensitive data — file contents, shell commands, conversation history — to your OTLP backend. | `false`   |
| `telemetry.sensitiveSpanAttributeMaxLength` | number  | Maximum JavaScript string length for each sensitive native OTel span attribute content payload. Must be between `1` and `104857600` (100 MiB). Set lower if your collector or backend rejects large attributes.                                                                          | `1048576` |
| `telemetry.outfile`                         | string  | Path to write telemetry to a file. When set, overrides OTLP export.                                                                                                                                                                                                                      |           |

#### outboundCorrelation

⚠️ **Security-relevant.** Controls what client-side correlation data Qwen Code writes into outbound LLM API requests — a separate consent decision from `telemetry.*`, which governs data flowing into your OWN observability backend. All values default to off.

| Setting                                        | Type    | Description                                                                                                                                                     | Default |
| ---------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `outboundCorrelation.propagateTraceContext`    | boolean | Inject W3C `traceparent` on outbound `fetch` requests and as a `TRACEPARENT` env var in shell child processes. Requires `telemetry.enabled: true`.              | `false` |
| `outboundCorrelation.allowDynamicHeaderValues` | boolean | Allow `customHeaders` values to contain runtime placeholders such as `${session_id}`, expanded per request. When off, such a value is dropped rather than sent. | `false` |

```json
{
  "outboundCorrelation": {
    "allowDynamicHeaderValues": true
  }
}
```

`allowDynamicHeaderValues` is only the consent switch. _Which_ hosts receive the
value and _what_ the header is called are decided where the header lives —
`modelProviders[].generationConfig.customHeaders`, see
[Dynamic values in `customHeaders`](model-providers.md#dynamic-values-in-customheaders).
That scoping is why there is no host allowlist here: you already chose the
endpoint when you wrote the provider's `baseUrl`, and providers that should not
send the header simply do not carry it.

If a provider entry has a placeholder while this is off, the header is dropped
and a warning naming both is printed at startup — so a gateway rejecting your
requests should never be a silent mystery.

**Privacy note:** an expanded value is a stable per-conversation identifier.
Only put one on a provider you already send your prompt content to.

### Example `settings.json`

Here is an example of a `settings.json` file with the nested structure, new as of v0.3.0:

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Shell History

The CLI keeps a history of shell commands you run. To avoid conflicts between different projects, this history is stored in a project-specific directory within your user's home folder.

- **Location:** `~/.qwen/tmp/<project_hash>/shell_history`
  - `<project_hash>` is a unique identifier generated from your project's root path.
  - The history is stored in a file named `shell_history`.

## Environment Variables & `.env` Files

Environment variables are a common way to configure applications, especially for sensitive information (like tokens) or for settings that might change between environments.

Qwen Code can automatically load environment variables from `.env` files.
For authentication-related variables (like `OPENAI_*`) and the recommended `.qwen/.env` approach, see **[Authentication](../configuration/auth)**.

> [!tip]
>
> **Environment Variable Exclusion:** Some environment variables (like `DEBUG` and `DEBUG_MODE`) are automatically excluded from project `.env` files by default to prevent interference with the CLI behavior. Variables from `.qwen/.env` files are never excluded by this default list. You can customize this behavior using the `advanced.excludedEnvVars`setting in your `settings.json` file.

> [!warning]
>
> **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `OPENSSL_CONF` (dlopens an attacker OpenSSL engine at startup), `NODE_REPL_EXTERNAL_MODULE`, `npm_config_node_gyp`, `npm_config_init_module`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `QWEN_CDP_MCP_COMMAND` (the command the daemon spawns as the browser-automation MCP adapter), `QWEN_SERVE_CDP_TUNNEL_OVER_WS` (switches that tunnel surface on), `DEV` (the dev-harness launch marker), the TLS trust-anchor variables (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`, `GIT_SSL_CAINFO`, `GIT_SSL_CAPATH`, `npm_config_cafile`, `npm_config_ca`, `npm_config_strict_ssl`, `PIP_CERT` — an attacker CA there, or `npm_config_strict_ssl=false`, would enable MITM of the token-bearing traffic a session's `git`/`npm`/`pip`/`curl` calls make), the git command-execution variables (`GIT_SSH_COMMAND`, `GIT_SSH`, `GIT_EXEC_PATH`, `GIT_TEMPLATE_DIR`, `GIT_ASKPASS`, `GIT_PROXY_COMMAND`, `GIT_EDITOR`, `GIT_SEQUENCE_EDITOR`, `GIT_EXTERNAL_DIFF`, `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG_COUNT`, `GIT_CONFIG_PARAMETERS` and the numbered `GIT_CONFIG_KEY_<n>`/`GIT_CONFIG_VALUE_<n>` pairs — git runs these on any session `git` invocation — and `XDG_CONFIG_HOME`, which redirects the `$XDG_CONFIG_HOME/git/config` git merges with `~/.gitconfig`), the curl/wget rc-file redirects (`CURL_HOME`, `WGETRC` — their rc files can install an attacker proxy or CA), `PIP_CONFIG_FILE` (redirects all of pip's configuration — `index-url`, `trusted-host`, proxy, or cert settings in an attacker file send session pip traffic or credentials to attacker infrastructure), `SSH_ASKPASS` (git/ssh execute it as the fallback passphrase-prompt program on an auth challenge), `LESSOPEN` and `LESSCLOSE` (`less` executes them as input preprocessors on every file a session views), the node-gyp interpreter-selection variables (`NODE_GYP_FORCE_PYTHON`, `npm_config_python`, `PYTHON` — run as the build Python during native-addon installs — and `npm_config_git`, run as npm's git binary), the editor and startup hooks (`VISUAL`, `EDITOR` — git's editor fallback chain, also spawned by the CLI's own external-editor flows — and `PYTHONSTARTUP`, which CPython executes at interactive startup), or `BROWSER` (the CLI execs it via the secure browser launcher), the sandbox confinement-decision variables (`QWEN_SANDBOX`, which decides whether confinement runs at all and which backend, `QWEN_SANDBOX_IMAGE`, which selects the container image the agent runs inside, `QWEN_SANDBOX_NET`, the network mode, and `QWEN_SANDBOX_PROXY_COMMAND`, which the bwrap backend executes through `bash -c` on the host), and the writable-root derivation variables (`XDG_CACHE_HOME`, `TMPDIR`, `TMP`, `TEMP` — the bwrap backend binds the directories they name read-write, so a project file pointing one inside your home directory would widen the confinement's write surface). Those stay settable from the shell environment or a user-level `.env`; unlike the loader list above they are rejected from project files only, so a value you export yourself is preserved. They are also frozen at boot from a user-level `.env`: a settings reload does not apply edits to them there — or their removal — until the process restarts. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses.

### Environment Variables Table

| Variable                                             | Description                                                                                                                                                                                                                                                                                                                                                                                                      | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `QWEN_HOME`                                          | Customizes the global configuration directory (default: `~/.qwen`). Accepts an absolute or relative path (relative paths are resolved from the current working directory). Leading `~` is expanded to the user's home directory.                                                                                                                                                                                 | Stores credentials, settings, memory, skills, and other global state. When set, project-level `.qwen/` directories are unaffected. An empty string is treated as unset.                                                                                                                                                                                                                                                                                                                                                               |
| `QWEN_RUNTIME_DIR`                                   | Overrides the runtime output directory (conversations, logs, todos). When unset, defaults to the `QWEN_HOME` directory.                                                                                                                                                                                                                                                                                          | Use this to separate ephemeral runtime data from persistent config. Useful when `QWEN_HOME` is on a shared/slow filesystem.                                                                                                                                                                                                                                                                                                                                                                                                           |
| `QWEN_USAGE_STATISTICS_ENABLED`                      | Set to `true` or `1` to enable usage statistics. Any other value is treated as disabling them.                                                                                                                                                                                                                                                                                                                   | Overrides the `privacy.usageStatisticsEnabled` setting. Defaults to enabled when neither is configured.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `QWEN_TELEMETRY_ENABLED`                             | Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it.                                                                                                                                                                                                                                                                                                                            | Overrides the `telemetry.enabled` setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `QWEN_TELEMETRY_TARGET`                              | Sets an informational label for the telemetry destination (`local` or `gcp`). Does not control routing; use `QWEN_TELEMETRY_OTLP_ENDPOINT` or `QWEN_TELEMETRY_OUTFILE` to configure where data is sent.                                                                                                                                                                                                          | Overrides the `telemetry.target` setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `QWEN_TELEMETRY_OTLP_ENDPOINT`                       | Sets the OTLP endpoint for telemetry.                                                                                                                                                                                                                                                                                                                                                                            | Overrides the `telemetry.otlpEndpoint` setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `QWEN_TELEMETRY_OTLP_PROTOCOL`                       | Sets the OTLP exporter protocol: `grpc` (gRPC/protobuf) or `http` (HTTP/JSON, `application/json`).                                                                                                                                                                                                                                                                                                               | Overrides the `telemetry.otlpProtocol` setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `QWEN_TELEMETRY_LOG_PROMPTS`                         | Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it.                                                                                                                                                                                                                                                                                                   | Overrides the `telemetry.logPrompts` setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `QWEN_TELEMETRY_USER_ID`                             | Sets a stable end-user identifier on interaction, LLM, Tool, and Agent spans as `gen_ai.user.id`. Prefer a pseudonymous value.                                                                                                                                                                                                                                                                                   | Overrides `telemetry.userId` after trimming. A blank value falls back to settings. This is process-wide and must not be used as per-request identity in a shared multi-user process.                                                                                                                                                                                                                                                                                                                                                  |
| `QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES`   | Set to `true` or `1` to attach verbatim user prompts, system prompts, tool I/O, and model responses to native OTel span attributes (and keep `function_args`, `error`, `error.message`, and `error_message` on log-to-span bridge spans, plus `prompt` / `request_text` / `response_text` when `telemetry.logPrompts` is also enabled). Any other value disables it.                                             | Overrides the `telemetry.includeSensitiveSpanAttributes` setting. ⚠️ Streams sensitive data to your OTLP backend.                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `QWEN_TELEMETRY_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH` | Sets the maximum JavaScript string length for each sensitive native OTel span attribute content payload. Must be a positive integer no greater than `104857600` (100 MiB).                                                                                                                                                                                                                                       | Overrides the `telemetry.sensitiveSpanAttributeMaxLength` setting. Default is `1048576` (1 MiB); lower it if your collector or backend rejects large span attributes.                                                                                                                                                                                                                                                                                                                                                                 |
| `QWEN_TELEMETRY_OUTFILE`                             | Sets the file path to write telemetry to. When set, overrides OTLP export.                                                                                                                                                                                                                                                                                                                                       | Overrides the `telemetry.outfile` setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `QWEN_SANDBOX`                                       | Alternative to the `sandbox` setting in `settings.json`.                                                                                                                                                                                                                                                                                                                                                         | Accepts `true`, `false`, `docker`, `podman`, or a custom command string.                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `QWEN_SANDBOX_IMAGE`                                 | Overrides sandbox image selection for Docker/Podman.                                                                                                                                                                                                                                                                                                                                                             | Takes precedence over `tools.sandboxImage`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `SEATBELT_PROFILE`                                   | (macOS specific) Switches the Seatbelt (`sandbox-exec`) profile on macOS.                                                                                                                                                                                                                                                                                                                                        | `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/serve/sandbox-macos-permissive-open.sb`) but allows other operations. `strict`: Uses a strict profile that declines operations by default. `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.qwen/` directory (e.g., `my-project/.qwen/sandbox-macos-custom.sb`).                                                    |
| `DEBUG` or `DEBUG_MODE`                              | (often used by underlying libraries or the CLI itself) Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting.                                                                                                                                                                                                                                                           | **Note:** These variables are automatically excluded from project `.env` files by default to prevent interference with the CLI behavior. Use `.qwen/.env` files if you need to set these for Qwen Code specifically.                                                                                                                                                                                                                                                                                                                  |
| `NO_COLOR`                                           | Set to any value to disable all color output in the CLI.                                                                                                                                                                                                                                                                                                                                                         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `FORCE_HYPERLINK`                                    | Override the OSC 8 clickable-link detection in the markdown renderer. Set to `1` (or any non-zero integer, or empty string) to force-enable; set to `0` or a non-numeric value such as `false` / `off` to force-disable. Honors `NO_COLOR` / `QWEN_DISABLE_HYPERLINKS` opt-outs above it.                                                                                                                        | Use this to opt into OSC 8 inside `tmux` / GNU `screen` (auto-detection refuses by default because the host terminal's capabilities are hidden behind the multiplexer). Requires `set -g allow-passthrough on` on tmux 3.3+. Also enables Hyper, which isn't auto-detected.                                                                                                                                                                                                                                                           |
| `QWEN_DISABLE_HYPERLINKS`                            | Set to `1` to hard-disable OSC 8 clickable hyperlinks in the markdown renderer even on terminals that auto-detect as capable.                                                                                                                                                                                                                                                                                    | Useful when a terminal advertises support but breaks on long URLs, or when piping output through an intermediary that mangles escape sequences. The renderer falls back to plain `label (url)` rendering.                                                                                                                                                                                                                                                                                                                             |
| `CLI_TITLE`                                          | Set to a string to customize the title of the CLI.                                                                                                                                                                                                                                                                                                                                                               |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `CODE_ASSIST_ENDPOINT`                               | Specifies the endpoint for the code assist server.                                                                                                                                                                                                                                                                                                                                                               | This is useful for development and testing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `QWEN_CODE_MAX_OUTPUT_TOKENS`                        | Overrides the default maximum output tokens per response. When not set, Qwen Code defaults to the model's declared output limit and, if a response is truncated, automatically escalates (64K floor) and recovers across turns. Set this to a specific value (e.g., `16000`) to use a fixed limit instead — useful for capacity-constrained self-hosted backends that want a lower per-request slot reservation. | Takes precedence over the model-limit default but is overridden by `samplingParams.max_tokens` in settings. Disables automatic escalation when set. Example: `export QWEN_CODE_MAX_OUTPUT_TOKENS=16000`                                                                                                                                                                                                                                                                                                                               |
| `QWEN_CODE_UNATTENDED_RETRY`                         | Set to `true` or `1` to enable persistent retry mode. When enabled, transient API capacity errors (HTTP 429 Rate Limit and 529 Overloaded) are retried indefinitely with exponential backoff (capped at 5 minutes per retry) and heartbeat keepalives every 30 seconds on stderr.                                                                                                                                | Designed for CI/CD pipelines and background automation where long-running tasks should survive temporary API outages. Must be set explicitly — `CI=true` alone does **not** activate this mode. See [Headless Mode](../features/headless#persistent-retry-mode) for details. Example: `export QWEN_CODE_UNATTENDED_RETRY=1`                                                                                                                                                                                                           |
| `QWEN_CODE_ACP_REPEATED_TOOL_FAILURE_GUARD`          | Operator rollout mode for ACP repeated tool-execution failure protection. Accepts `off`, `shadow`, `warn`, or `enforce`; missing or invalid values default to `shadow`.                                                                                                                                                                                                                                          | Applies only to interactive foreground ACP prompts; channel-driven and automatic routes remain off. Project and workspace environment files cannot set this operator policy. Shadow leaves model continuation and messages unchanged but adds the queued-prompt watch flag to `craft/drainMidTurnQueue`; every non-off mode requires reliable queued-prompt state. Non-empty invalid values emit a diagnostic; export the variable in the process environment or a user-level file.                                                   |
| `QWEN_CODE_PROFILE_STARTUP`                          | Set to `1` to enable startup performance profiling. Writes a JSON timing report to `~/.qwen/startup-perf/` with per-phase durations.                                                                                                                                                                                                                                                                             | Only active inside the sandbox child process (or with `QWEN_CODE_PROFILE_STARTUP_OUTER=1`). Zero overhead when not set. Example: `export QWEN_CODE_PROFILE_STARTUP=1`                                                                                                                                                                                                                                                                                                                                                                 |
| `QWEN_CODE_PROFILE_STARTUP_OUTER`                    | Set to `1` together with `QWEN_CODE_PROFILE_STARTUP=1` to also collect a startup profile in the outer (pre-sandbox) process. Outer-process reports get an `outer-` filename prefix to keep them distinct from the sandbox child's report.                                                                                                                                                                        | Off by default — only the sandbox child collects, to avoid duplicate reports. Useful for local development where the cli isn't relaunched into a sandbox.                                                                                                                                                                                                                                                                                                                                                                             |
| `QWEN_CODE_PROFILE_STARTUP_NO_HEAP`                  | Set to `1` together with `QWEN_CODE_PROFILE_STARTUP=1` to skip the per-checkpoint `process.memoryUsage()` snapshots. Useful when measuring the profiler's own Heisenberg overhead.                                                                                                                                                                                                                               | Off by default. Heap snapshots cost ~50 µs each (well below 1% of total startup) so most users should leave this alone.                                                                                                                                                                                                                                                                                                                                                                                                               |
| `QWEN_CODE_LEGACY_MCP_BLOCKING`                      | Set to `1` to restore the pre-progressive-MCP behavior where `Config.initialize()` waits synchronously for every configured MCP server's discover handshake before returning.                                                                                                                                                                                                                                    | Off by default. Modern qwen-code lets MCP servers come online in the background while the UI is already interactive; the model sees each batch of new tools within ~16 ms of the server settling. This flag is kept as a rollback escape hatch for ≥ 1 release. Example: `export QWEN_CODE_LEGACY_MCP_BLOCKING=1`                                                                                                                                                                                                                     |
| `QWEN_CODE_MODELS_DEV`                               | Set to `off` to ignore the bundled [models.dev](https://models.dev) model catalog and resolve context windows, output limits, and input modalities from the built-in model-name tables only.                                                                                                                                                                                                                     | On by default. Only the exact value `off` disables the catalog; it also suppresses the background refresh below. Project-level `.env` files and `settings.json` `env` cannot set this key, and it cannot be changed by a reload; use the shell environment. Example: `export QWEN_CODE_MODELS_DEV=off`                                                                                                                                                                                                                                |
| `QWEN_CODE_MODELS_DEV_REFRESH`                       | Set to `off` to stop the daily models.dev download into `~/.qwen/model-registry.json` (air-gapped or proxy-restricted environments). Refreshed data is available to new CLI processes; running processes keep their selected catalog.                                                                                                                                                                            | On by default (24-hour interval). Stops only the download — a previously downloaded `~/.qwen/model-registry.json` is used while its `fetchedAt` timestamp is newer than the snapshot bundled with the installed release; after an upgrade ships a newer snapshot the bundled data wins until you refresh again. Use `QWEN_CODE_MODELS_DEV=off` to ignore downloaded catalog data entirely. Project-level `.env` files and `settings.json` `env` cannot set this key, and it cannot be changed by a reload; use the shell environment. |
| `QWEN_CODE_MODELS_DEV_URL`                           | Replaces the models.dev download URL with a full mirror of `https://models.dev/api.json` (same provider filtering applies). To add or correct individual models instead, use explicit `modelProviders` generation settings.                                                                                                                                                                                      | Unset — downloads from `https://models.dev/api.json`. Project-level `.env` files and `settings.json` `env` cannot set this key; use the shell environment or a user-level `.env` file. Example: `export QWEN_CODE_MODELS_DEV_URL=https://mirror.corp/api.json`                                                                                                                                                                                                                                                                        |
| `QWEN_CODE_LEGACY_ERASE_LINES`                       | `=1` force-disables the terminal redraw optimizer (restores per-line erase sequences); `=0` force-enables it even on WSL, where it is skipped by default because ConPTY mishandles the optimizer's batched cursor moves (issue #7634). Unset = platform default (skip when `WSL_DISTRO_NAME` or `WSL_INTEROP` is set).                                                                                           | Escape hatch for streaming-output regressions. Because it is read from the environment, launchers that scrub env (e.g. `sudo`) drop it too — pass it at launch instead: `sudo QWEN_CODE_LEGACY_ERASE_LINES=1 qwen`. Example: `export QWEN_CODE_LEGACY_ERASE_LINES=1`                                                                                                                                                                                                                                                                  |

When both user-level `.env` files define the same variable, the Qwen-specific
file wins: `<QWEN_HOME>/.env` (or `~/.qwen/.env` when `QWEN_HOME` is unset) is
loaded before `~/.env`, and existing environment values are not overwritten.

### Standalone update download source

Set `QWEN_UPDATE_BASE_URL` to use a custom HTTPS release root for standalone updates:

```bash
export QWEN_UPDATE_BASE_URL="https://downloads.example.com/qwen-code"
qwen update
```

For version `0.23.0`, the updater downloads the platform archive, `SHA256SUMS`, and `SHA256SUMS.sig` from `<base-url>/v0.23.0/`. Host these files using the same names and directory layout as an official release. Existing checksum and signature checks still apply, including `QWEN_REQUIRE_SIGNATURE=1` when a signature is required.

The URL must use HTTPS and cannot contain credentials, a query string, or a fragment. Surrounding whitespace and trailing slashes are removed. An empty or whitespace-only value preserves the built-in download sources and their fallback order. When a custom root is set, a failed download does not fall back to the built-in sources.

Configure this variable in the launching shell or a user-level `.env` file. It is rejected from project `.env` and `.qwen/.env` files and from the top-level `settings.json` `env` section at every scope. A user-level `.env` value is loaded at startup; restart Qwen Code after changing it.

This setting applies to `qwen update`, `/update`, and automatic standalone updates. It does not change npm registry version discovery. It is separate from the installer's `QWEN_INSTALL_BASE_URL`, which points directly to a version-specific directory.

## Command-Line Arguments

Arguments passed directly when running the CLI can override other configurations for that specific session.

For sandbox image selection, precedence is:
`--sandbox-image` > `QWEN_SANDBOX_IMAGE` > `tools.sandboxImage` > built-in default image.

### Command-Line Arguments Table

| Argument                     | Alias | Description                                                                                                                                                                                                                                                                                          | Possible Values                                | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ---------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--model`                    | `-m`  | Specifies the Qwen model to use for this session.                                                                                                                                                                                                                                                    | Model name                                     | Example: `npm start -- --model qwen3-coder-plus`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `--prompt`                   | `-p`  | Used to pass a prompt directly to the command. This invokes Qwen Code in a non-interactive mode.                                                                                                                                                                                                     | Your prompt text                               | For scripting examples, use the `--output-format json` flag to get structured output.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `--prompt-interactive`       | `-i`  | Starts an interactive session with the provided prompt as the initial input.                                                                                                                                                                                                                         | Your prompt text                               | The prompt is processed within the interactive session, not before it. Cannot be used when piping input from stdin. Example: `qwen -i "explain this code"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `--system-prompt`            |       | Overrides the built-in main session system prompt for this run.                                                                                                                                                                                                                                      | Your prompt text                               | Loaded context files such as `QWEN.md` are still appended after this override. Can be combined with `--append-system-prompt`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `--append-system-prompt`     |       | Appends extra instructions to the main session system prompt for this run.                                                                                                                                                                                                                           | Your prompt text                               | Applied after the built-in prompt and loaded context files. Can be combined with `--system-prompt`. See [Headless Mode](../features/headless) for examples.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `--output-style`             |       | Selects the output style that shapes how responses are written for this run.                                                                                                                                                                                                                         | Style name                                     | `Concise`, `Proactive`, `Explanatory`, `Learning`, the name of a [custom style](../features/output-styles#custom-styles), or `default` for no style (case-insensitive). Overrides the `general.outputStyle` setting. An unknown name prints a warning and the session starts with the default style. Has no effect when `--system-prompt` or `QWEN_SYSTEM_MD` replaces the built-in prompt. See [Headless Mode](../features/headless) for examples.                                                                                                                                                                                                           |
| `--output-format`            | `-o`  | Specifies the format of the CLI output for non-interactive mode.                                                                                                                                                                                                                                     | `text`, `json`, `stream-json`                  | `text`: (Default) The standard human-readable output. `json`: A machine-readable JSON output emitted at the end of execution. `stream-json`: Streaming JSON messages emitted as they occur during execution. For structured output and scripting, use the `--output-format json` or `--output-format stream-json` flag. See [Headless Mode](../features/headless) for detailed information.                                                                                                                                                                                                                                                                   |
| `--input-format`             |       | Specifies the format consumed from standard input.                                                                                                                                                                                                                                                   | `text`, `stream-json`                          | `text`: (Default) Standard text input from stdin or command-line arguments. `stream-json`: JSON message protocol via stdin for bidirectional communication. Requirement: `--input-format stream-json` requires `--output-format stream-json` to be set. When using `stream-json`, stdin is reserved for protocol messages. See [Headless Mode](../features/headless) for detailed information.                                                                                                                                                                                                                                                                |
| `--include-partial-messages` |       | Include partial assistant messages when using `stream-json` output format. When enabled, emits stream events (message_start, content_block_delta, etc.) as they occur during streaming.                                                                                                              |                                                | Default: `false`. Requirement: Requires `--output-format stream-json` to be set. See [Headless Mode](../features/headless) for detailed information about stream events.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `--sandbox`                  | `-s`  | Enables sandbox mode for this session.                                                                                                                                                                                                                                                               |                                                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--sandbox-image`            |       | Sets the sandbox image URI.                                                                                                                                                                                                                                                                          |                                                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--debug`                    | `-d`  | Enables debug mode for this session, providing more verbose output.                                                                                                                                                                                                                                  |                                                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--help`                     | `-h`  | Displays help information about command-line arguments.                                                                                                                                                                                                                                              |                                                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--yolo`                     |       | Enables YOLO mode, which automatically approves all tool calls.                                                                                                                                                                                                                                      |                                                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--approval-mode`            |       | Sets the approval mode for tool calls.                                                                                                                                                                                                                                                               | `plan`, `default`, `auto-edit`, `auto`, `yolo` | Supported modes: `plan`: Analyze only—do not modify files or execute commands. `default`: Require approval for file edits or shell commands (default behavior). `auto-edit`: Automatically approve edit tools (`edit`, `write_file`, `notebook_edit`) while prompting for others. `auto`: LLM classifier auto-approves safe actions and blocks risky ones. `yolo`: Automatically approve all tool calls (equivalent to `--yolo`). Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach. Example: `qwen --approval-mode auto-edit`<br>See more about [Approval Mode](../features/approval-mode). |
| `--allowed-tools`            |       | A comma-separated list of tool names that will bypass the confirmation dialog.                                                                                                                                                                                                                       | Tool names                                     | Example: `qwen --allowed-tools "Shell(git status)"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `--disabled-slash-commands`  |       | Slash command names to hide/disable (comma-separated or repeated). Unioned with the `slashCommands.disabled` setting and the `QWEN_DISABLED_SLASH_COMMANDS` environment variable. Matched case-insensitively against the final command name, with the same either-spelling rule for a Skill command. | Command names                                  | Example: `qwen --disabled-slash-commands "auth,mcp,extensions"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--telemetry`                |       | Enables [telemetry](../../developers/development/telemetry.md).                                                                                                                                                                                                                                      |                                                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--telemetry-target`         |       | Sets the telemetry target.                                                                                                                                                                                                                                                                           |                                                | See [telemetry](../../developers/development/telemetry.md) for more information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `--telemetry-otlp-endpoint`  |       | Sets the OTLP endpoint for telemetry.                                                                                                                                                                                                                                                                |                                                | See [telemetry](../../developers/development/telemetry.md) for more information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `--telemetry-otlp-protocol`  |       | Sets the OTLP protocol for telemetry (`grpc` or `http`).                                                                                                                                                                                                                                             |                                                | Defaults to `grpc`. See [telemetry](../../developers/development/telemetry.md) for more information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `--telemetry-log-prompts`    |       | Enables logging of prompts for telemetry.                                                                                                                                                                                                                                                            |                                                | See [telemetry](../../developers/development/telemetry.md) for more information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `--acp`                      |       | Enables ACP mode (Agent Client Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed).                                                                                                                                                                                         |                                                | Stable. Replaces the deprecated `--experimental-acp` flag.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `--experimental-lsp`         |       | Enables experimental [LSP (Language Server Protocol)](../features/lsp) feature for code intelligence (go-to-definition, find references, diagnostics, etc.).                                                                                                                                         |                                                | Experimental. Requires language servers to be installed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `--extensions`               | `-e`  | Specifies a list of extensions to use for the session.                                                                                                                                                                                                                                               | Extension names                                | If not provided, all available extensions are used. Use the special term `qwen -e none` to disable all extensions. Example: `qwen -e my-extension -e my-other-extension`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `--list-extensions`          | `-l`  | Lists all available extensions and exits.                                                                                                                                                                                                                                                            |                                                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--proxy`                    |       | Sets the proxy for the CLI.                                                                                                                                                                                                                                                                          | Proxy URL                                      | Example: `--proxy http://localhost:7890`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `--include-directories`      |       | Includes additional directories in the workspace for multi-directory support.                                                                                                                                                                                                                        | Directory paths                                | Can be specified multiple times or as comma-separated values. Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2`                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `--screen-reader`            |       | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers.                                                                                                                                                                                                      |                                                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--version`                  |       | Displays the version of the CLI.                                                                                                                                                                                                                                                                     |                                                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--openai-logging`           |       | Enables logging of OpenAI API calls for debugging and analysis.                                                                                                                                                                                                                                      |                                                | This flag overrides the `enableOpenAILogging` setting in `settings.json`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `--openai-logging-dir`       |       | Sets a custom directory path for OpenAI API logs.                                                                                                                                                                                                                                                    | Directory path                                 | This flag overrides the `openAILoggingDir` setting in `settings.json`. Supports absolute paths, relative paths, and `~` expansion. Example: `qwen --openai-logging-dir "~/qwen-logs" --openai-logging`                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

## Context Files (Hierarchical Instructional Context)

While not strictly configuration for the CLI's _behavior_, context files (defaulting to `QWEN.md` but configurable via the `context.fileName` setting) are crucial for configuring the _instructional context_ (also referred to as "memory"). This powerful feature allows you to give project-specific instructions, coding style guides, or any relevant background information to the AI, making its responses more tailored and accurate to your needs. The CLI includes UI elements, such as an indicator in the footer showing the number of loaded context files, to keep you informed about the active context.

- **Purpose:** These Markdown files contain instructions, guidelines, or context that you want the Qwen model to be aware of during your interactions. The system is designed to manage this instructional context hierarchically.

### Example Context File Content (e.g. `QWEN.md`)

Here's a conceptual example of what a context file at the root of a TypeScript project might contain:

```
# Project: My Awesome TypeScript Library

## General Instructions:
- When generating new TypeScript code, please follow the existing coding style.
- Ensure all new functions and classes have JSDoc comments.
- Prefer functional programming paradigms where appropriate.
- All code should be compatible with TypeScript 5.0 and Node.js 22+.

## Coding Style:
- Use 2 spaces for indentation.
- Interface names should be prefixed with `I` (e.g., `IUserService`).
- Private class members should be prefixed with an underscore (`_`).
- Always use strict equality (`===` and `!==`).

## Specific Component: `src/api/client.ts`
- This file handles all outbound API requests.
- When adding new API call functions, ensure they include robust error handling and logging.
- Use the existing `fetchWithRetry` utility for all GET requests.

## Regarding Dependencies:
- Avoid introducing new external dependencies unless absolutely necessary.
- If a new dependency is required, please state the reason.
```

This example demonstrates how you can provide general project context, specific coding conventions, and even notes about particular files or components. The more relevant and precise your context files are, the better the AI can assist you. Project-specific context files are highly encouraged to establish conventions and context.

- **Hierarchical Loading and Precedence:** The CLI implements a hierarchical memory system by loading context files (e.g., `QWEN.md`) from several locations. Content from files lower in this list (more specific) typically overrides or supplements content from files higher up (more general). The exact concatenation order and final context can be inspected from the `/memory` dialog. The typical loading order is:
  1. **Global Context File:**
     - Location: `~/.qwen/<configured-context-filename>` (e.g., `~/.qwen/QWEN.md` in your user home directory).
     - Scope: Provides default instructions for all your projects.
  2. **Project Root & Ancestors Context Files:**
     - Location: The CLI searches for the configured context file in the current working directory and then in each parent directory up to either the project root (identified by a `.git` folder) or your home directory.
     - Scope: Provides context relevant to the entire project or a significant portion of it.
- **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context.
- **Importing Content:** You can modularize your context files by importing other Markdown files using the `@path/to/file.md` syntax. For more details, see the [Memory documentation](../features/memory.md).
- **Commands for Memory Management:**
  - Use `/memory` to open the memory management dialog.
  - Refresh memory from the dialog to re-scan and reload context files from all configured locations.
  - See the [Commands documentation](../features/commands.md) for full details on the `/memory` command.

By understanding and utilizing these configuration layers and the hierarchical nature of context files, you can effectively manage the AI's memory and tailor Qwen Code's responses to your specific needs and projects.

## Sandbox

Qwen Code can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system.

[Sandbox](../features/sandbox) is disabled by default, but you can enable it in a few ways:

- Using `--sandbox` or `-s` flag.
- Setting `QWEN_SANDBOX` environment variable.
- Setting `tools.sandbox` in settings.

> ⚠️ **`--yolo` does _not_ automatically enable a sandbox.** YOLO mode only auto-approves tool calls; sandboxing must still be opted into via `--sandbox`, `QWEN_SANDBOX`, or `tools.sandbox`. In headless / non-interactive runs with `--yolo` (or `--approval-mode=yolo`) and no sandbox, the model can execute shell, write, and edit tools at the current process's privilege level — Qwen Code prints a warning to stderr in that case. Suppress with `QWEN_CODE_SUPPRESS_YOLO_WARNING=1` once you've reviewed the trade-off.

By default, it uses a pre-built `qwen-code-sandbox` Docker image.

For project-specific sandboxing needs, you can create a custom Dockerfile at `.qwen/sandbox.Dockerfile` in your project's root directory. This Dockerfile can be based on the base sandbox image:

```
FROM qwen-code-sandbox
# Add your custom dependencies or configurations here
# For example:
# RUN apt-get update && apt-get install -y some-package
# COPY ./my-config /app/my-config
```

When `.qwen/sandbox.Dockerfile` exists, you can use `BUILD_SANDBOX` environment variable when running Qwen Code to automatically build the custom sandbox image:

```
BUILD_SANDBOX=1 qwen -s
```

## Usage Statistics

To help us improve Qwen Code, we collect anonymized usage statistics. This data helps us understand how the CLI is used, identify common issues, and prioritize new features.

**What we collect:**

- **Tool Calls:** We log the names and categories (native or MCP) of the tools that are called, their terminal status (success, error, or cancelled), and how long they take to execute. We do not collect the arguments passed to the tools or any data returned by them.
- **API Requests:** We log the model used for each request, the duration of the request, and whether it was successful. We do not collect the content of the prompts or responses.
- **Session Information:** We collect information about the configuration of the CLI, such as the enabled tools and the approval mode.

**What we DON'T collect:**

- **Personally Identifiable Information (PII):** We do not collect any personal information, such as your name, email address, or API keys.
- **Prompt and Response Content:** We do not log the content of your prompts or the responses from the model.
- **File Content:** We do not log the content of any files that are read or written by the CLI.

**How to opt out:**

You can opt out of usage statistics collection at any time by setting the `usageStatisticsEnabled` property to `false` under the `privacy` category in your `settings.json` file:

```
{
  "privacy": {
    "usageStatisticsEnabled": false
  }
}
```

Alternatively, set `QWEN_USAGE_STATISTICS_ENABLED=false` (or `0`) in the
environment. The environment variable takes precedence over the setting.
Restart Qwen Code after changing either value.

> [!note]
>
> When usage statistics are enabled, events are sent to an Alibaba Cloud RUM collection endpoint.

---

### Users Configuration Themes (`docs/users/configuration/themes.md`)

# Themes

Qwen Code supports a variety of themes to customize its color scheme and appearance. You can change the theme to suit your preferences via the `/theme` command or the `"ui.theme"` configuration setting.

## Available Themes

Qwen Code comes with a selection of pre-defined themes, which you can list using the `/theme` command within the CLI:

- **Dark Themes:**
  - `ANSI`
  - `Atom One`
  - `Ayu`
  - `Default`
  - `Dracula`
  - `GitHub`
  - `Qwen Dark`
  - `Shades Of Purple`
- **Light Themes:**
  - `ANSI Light`
  - `Ayu Light`
  - `Default Light`
  - `GitHub Light`
  - `Google Code`
  - `Qwen Light`
  - `Xcode`

### Changing Themes

1.  Enter `/theme` into Qwen Code.
2.  A dialog or selection prompt appears, listing the available themes.
3.  Using the arrow keys, select a theme. Some interfaces might offer a live preview or highlight as you select.
4.  Confirm your selection to apply the theme.

**Note:** If a theme is defined in your `settings.json` file (either by name or by a file path), you must remove the `"ui.theme"` setting from the file before you can change the theme using the `/theme` command.

### Theme Persistence

Selected themes are saved in Qwen Code's [configuration](../configuration/settings) so your preference is remembered across sessions.

---

## Auto Theme Detection

When the theme is set to `"auto"` (or left unset), Qwen Code automatically detects whether your terminal uses a dark or light background and selects the matching Qwen theme (`Qwen Dark` or `Qwen Light`).

### How to enable

Set the theme to `"auto"` in `settings.json`:

```json
{
  "ui": {
    "theme": "auto"
  }
}
```

Or select **Auto** in the `/theme` dialog. This is the default behavior when no theme is explicitly configured.

### Detection methods

Qwen Code uses multiple detection methods in a fallback chain. At startup (async path), the order is:

| Priority | Method                  | Platform   | How it works                                                                                         |
| -------- | ----------------------- | ---------- | ---------------------------------------------------------------------------------------------------- |
| 1        | `COLORFGBG`             | All        | Reads the `COLORFGBG` environment variable (set by terminals like iTerm2, rxvt, Konsole)             |
| 2        | OSC 11                  | All (TTY)  | Sends an `ESC]11;?` query to the terminal and parses the background color from the response (~200ms) |
| 3        | macOS system appearance | macOS only | Runs `defaults read -g AppleInterfaceStyle` to check if macOS Dark Mode is active                    |
| 4        | Default                 | All        | Falls back to dark theme if no method succeeds                                                       |

The first method that returns a result wins. The detected value is cached for the session so subsequent theme resolutions (e.g. reselecting Auto in the `/theme` dialog) stay consistent.

### When to use Auto

- **Most users** — Auto works well if your terminal background matches your OS appearance or if your terminal sets `COLORFGBG` / supports OSC 11.
- **tmux / screen users** — OSC 11 may not pass through multiplexers. Detection falls back to `COLORFGBG` or macOS system appearance. If neither is available, the default dark theme is used. Set a specific theme if auto-detection gives the wrong result.
- **SSH sessions** — detection depends on the remote environment. If `COLORFGBG` is not forwarded and the remote terminal doesn't respond to OSC 11, the default dark theme is used.

---

## Custom Color Themes

Qwen Code allows you to create your own custom color themes by specifying them in your `settings.json` file. This gives you full control over the color palette used in the CLI.

### How to Define a Custom Theme

Add a `customThemes` block to your user, project, or system `settings.json` file. Each custom theme is defined as an object with a unique name and a set of color keys. For example:

```json
{
  "ui": {
    "customThemes": {
      "MyCustomTheme": {
        "name": "MyCustomTheme",
        "type": "custom",
        "Background": "#181818",
        ...
      }
    }
  }
}
```

**Color keys:**

- `Background`
- `Foreground`
- `LightBlue`
- `AccentBlue`
- `AccentPurple`
- `AccentCyan`
- `AccentGreen`
- `AccentYellow`
- `AccentRed`
- `Comment`
- `Gray`
- `DiffAdded` (optional, for added lines in diffs)
- `DiffRemoved` (optional, for removed lines in diffs)
- `DiffModified` (optional, for modified lines in diffs)

**Required Properties:**

- `name` (must match the key in the `customThemes` object and be a string)
- `type` (must be the string `"custom"`)
- `Background`
- `Foreground`
- `LightBlue`
- `AccentBlue`
- `AccentPurple`
- `AccentCyan`
- `AccentGreen`
- `AccentYellow`
- `AccentRed`
- `Comment`
- `Gray`

You can use either hex codes (e.g., `#FF0000`) **or** standard CSS color names (e.g., `coral`, `teal`, `blue`) for any color value. See [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#color_keywords) for a full list of supported names.

You can define multiple custom themes by adding more entries to the `customThemes` object.

### Loading Themes from a File

In addition to defining custom themes in `settings.json`, you can also load a theme directly from a JSON file by specifying the file path in your `settings.json`. This is useful for sharing themes or keeping them separate from your main configuration.

To load a theme from a file, set the `ui.theme` property in your `settings.json` to the path of your theme file:

```json
{
  "ui": {
    "theme": "/path/to/your/theme.json"
  }
}
```

The theme file must be a valid JSON file that follows the same structure as a custom theme defined in `settings.json`.

**Example `my-theme.json`:**

```json
{
  "name": "My File Theme",
  "type": "custom",
  "Background": "#282A36",
  "Foreground": "#F8F8F2",
  "LightBlue": "#82AAFF",
  "AccentBlue": "#61AFEF",
  "AccentPurple": "#BD93F9",
  "AccentCyan": "#8BE9FD",
  "AccentGreen": "#50FA7B",
  "AccentYellow": "#F1FA8C",
  "AccentRed": "#FF5555",
  "Comment": "#6272A4",
  "Gray": "#ABB2BF",
  "DiffAdded": "#A6E3A1",
  "DiffRemoved": "#F38BA8",
  "DiffModified": "#89B4FA",
  "GradientColors": ["#4796E4", "#847ACE", "#C3677F"]
}
```

**Security Note:** For your safety, Qwen Code will only load theme files that are located within your home directory. If you attempt to load a theme from outside your home directory, a warning will be displayed and the theme will not be loaded. This is to prevent loading potentially malicious theme files from untrusted sources.

### Example Custom Theme

<img src="https://gw.alicdn.com/imgextra/i1/O1CN01Em30Hc1jYXAdIgls3_!!6000000004560-2-tps-1009-629.png" alt=" " style="zoom:100%;text-align:center;margin: 0 auto;" />

### Using Your Custom Theme

- Select your custom theme using the `/theme` command in Qwen Code. Your custom theme will appear in the theme selection dialog.
- Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui` object in your `settings.json`.
- Custom themes can be set at the user, project, or system level, and follow the same [configuration precedence](../configuration/settings) as other settings.

## Themes Preview

|  Dark Theme  |                                                                                Preview                                                                                |  Light Theme  |                                                                                Preview                                                                                |
| :----------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|     ANSI     |     <img src="https://gw.alicdn.com/imgextra/i2/O1CN01ZInJiq1GdSZc9gHsI_!!6000000000645-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" />     |  ANSI Light   |     <img src="https://gw.alicdn.com/imgextra/i2/O1CN01IiJQFC1h9E3MXQj6W_!!6000000004234-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" />     |
| Atom OneDark |     <img src="https://gw.alicdn.com/imgextra/i2/O1CN01Zlx1SO1Sw21SkTKV3_!!6000000002310-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" />     |   Ayu Light   | <img src="https://gw.alicdn.com/imgextra/i3/O1CN01zEUc1V1jeUJsnCgQb_!!6000000004573-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
|     Ayu      | <img src="https://gw.alicdn.com/imgextra/i3/O1CN019upo6v1SmPhmRjzfN_!!6000000002289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | Default Light | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01RHjrEs1u7TXq3M6l3_!!6000000005990-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
|   Default    |     <img src="https://gw.alicdn.com/imgextra/i4/O1CN016pIeXz1pFC8owmR4Q_!!6000000005330-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" />     | GitHub Light  | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01US2b0g1VETCPAVWLA_!!6000000002621-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
|   Dracula    |     <img src="https://gw.alicdn.com/imgextra/i4/O1CN016htnWH20c3gd2LpUR_!!6000000006869-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" />     |  Google Code  | <img src="https://gw.alicdn.com/imgextra/i1/O1CN01Ng29ab23iQ2BuYKz8_!!6000000007289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
|    GitHub    | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01fFCRda1IQIQ9qDNqv_!!6000000000887-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |     Xcode     | <img src="https://gw.alicdn.com/imgextra/i1/O1CN010E3QAi1Huh5o1E9LN_!!6000000000818-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |

---

### Users Configuration Trusted Folders (`docs/users/configuration/trusted-folders.md`)

# Trusted Folders

The Trusted Folders feature is a security setting that gives you control over which projects can use the full capabilities of the Qwen Code. It prevents potentially malicious code from running by asking you to approve a folder before the CLI loads any project-specific configurations from it.

## Enabling the Feature

The Trusted Folders feature is **disabled by default**. To use it, you must first enable it in your settings.

Add the following to your user `settings.json` file:

```json
{
  "security": {
    "folderTrust": {
      "enabled": true
    }
  }
}
```

## How It Works: The Trust Dialog

Once the feature is enabled, the first time you run the Qwen Code from a folder, a dialog will automatically appear, prompting you to make a choice:

- **Trust folder**: Grants full trust to the current folder (e.g. `my-project`).
- **Trust parent folder**: Grants trust to the parent directory (e.g. `safe-projects`), which automatically trusts all of its subdirectories as well. This is useful if you keep all your safe projects in one place.
- **Don't trust**: Marks the folder as untrusted. The CLI will operate in a restricted "safe mode."

Your choice is saved in a central file (`~/.qwen/trustedFolders.json`), so you will only be asked once per folder.

The feature fails closed: until you make a choice, a folder counts as **untrusted**, not as trusted-by-default. If that file is missing — a new machine, a restored home directory, a synced dotfiles setup — every folder that no higher-priority signal decides (see "The Trust Check Process (Advanced)" below) starts untrusted, including folders you trusted before. When a trust check reaches the file rules, an unreadable file, invalid JSONC syntax, or a non-object document is a hard configuration error. A failed cached load makes the CLI stop with "Please fix the configuration file and try again." and the daemon's v1 trust checks answer `500 trusted_folders_invalid`; a prior IDE trust decision can bypass these file-based checks. The grant writer still validates the file before persisting a rule. Repair or remove the file by hand, then restart the daemon to clear the failed load. Comments and trailing commas are accepted. Invalid trust-level values are detected by the grant writer and v2 policy reader, but are not validated by the legacy v1 reader; v2 reports policy errors as `200` with `configured.state: "error"`. A file that becomes invalid after the cached load can instead fail during the grant write with `500 internal_error`, or during post-write policy verification with `409 trust_grant_ineffective`.

## Why Trust Matters: The Impact of an Untrusted Workspace

When a folder is **untrusted**, the Qwen Code runs in a restricted "safe mode" to protect you. In this mode, the following features are disabled:

1.  **Workspace Settings are Ignored**: The CLI will **not** load the `.qwen/settings.json` file from the project. This prevents the loading of custom tools and other potentially dangerous configurations.

2.  **Environment Variables are Ignored**: The CLI will **not** load any `.env` files from the project.

3.  **Extension Management is Restricted**: You **cannot install, update, or uninstall** extensions.

4.  **Tool Auto-Acceptance is Disabled**: You will always be prompted before any tool is run, even if you have auto-acceptance enabled globally.

5.  **Automatic Memory Loading is Disabled**: The CLI will not automatically load files into context from directories specified in local settings.

Granting trust to a folder unlocks the full functionality of the Qwen Code for that workspace.

## Managing Your Trust Settings

If you need to change a decision or see all your settings, you have a couple of options:

- **Change the Current Folder's Trust**: Run the `/permissions` command from within the CLI. This will bring up the same interactive dialog, allowing you to change the trust level for the current folder.

- **Trust a Workspace From the Web Shell**: Open the workspaces overview and use the **Trust** action on an untrusted workspace. This records the same decision without needing a terminal, which is the way back if every workspace came up untrusted because no rule decided them. The action appears only when the connected daemon advertises `workspace_trust_grant`, which it does only where trust hot-reload applies the decision to the running runtime — a daemon that serves the grant routes without hot-reload hides the action. There, a grant recorded through that daemon's own API updates the trust file and v1 trust status immediately, but startup-bound runtime gates require a restart. A decision recorded by a separate terminal process or a file edit is not immediately visible to the daemon's cached v1 status; restart the daemon to load it. Where the action is available, the workspace finishes becoming trusted once the daemon has rebuilt its runtime, which takes a moment.

- **View All Trust Rules**: To see a complete list of all your trusted and untrusted folder rules, you can inspect the contents of the `~/.qwen/trustedFolders.json` file in your home directory.

## The Trust Check Process (Advanced)

For advanced users, it's helpful to know the exact order of operations for how trust is determined:

1.  **IDE Trust Signal**: If you are using the [IDE Integration](../ide-integration/ide-integration), the CLI first asks the IDE if the workspace is trusted. The IDE's response takes highest priority.

2.  **Local Trust File**: If the IDE is not connected, the CLI checks the central `~/.qwen/trustedFolders.json` file.

---

### Users Extension Extension Releasing (`docs/users/extension/extension-releasing.md`)

# Extension Releasing

There are three primary ways of releasing extensions to users:

- [Git repository](#releasing-through-a-git-repository)
- [Github Releases](#releasing-through-github-releases)
- [npm Registry](#releasing-through-npm-registry)

Git repository releases tend to be the simplest and most flexible approach, while GitHub releases can be more efficient on initial install as they are shipped as single archives instead of requiring a git clone which downloads each file individually. Github releases may also contain platform specific archives if you need to ship platform specific binary files. npm registry releases are ideal for teams that already use npm for package distribution, especially with private registries.

## Releasing through a git repository

This is the most flexible and simple option. All you need to do us create a publicly accessible git repo (such as a public github repository) and then users can install your extension using `qwen extensions install <your-repo-uri>`, or for a GitHub repository they can use the simplified `qwen extensions install <org>/<repo>` format. They can optionally depend on a specific ref (branch/tag/commit) using the `--ref=<some-ref>` argument, this defaults to the default branch.

Whenever commits are pushed to the ref that a user depends on, they will be prompted to update the extension. Note that this also allows for easy rollbacks, the HEAD commit is always treated as the latest version regardless of the actual version in the `qwen-extension.json` file.

### Managing release channels using a git repository

Users can depend on any ref from your git repo, such as a branch or tag, which allows you to manage multiple release channels.

For instance, you can maintain a `stable` branch, which users can install this way `qwen extensions install <your-repo-uri> --ref=stable`. Or, you could make this the default by treating your default branch as your stable release branch, and doing development in a different branch (for instance called `dev`). You can maintain as many branches or tags as you like, providing maximum flexibility for you and your users.

Note that these `ref` arguments can be tags, branches, or even specific commits, which allows users to depend on a specific version of your extension. It is up to you how you want to manage your tags and branches.

### Example releasing flow using a git repo

While there are many options for how you want to manage releases using a git flow, we recommend treating your default branch as your "stable" release branch. This means that the default behavior for `qwen extensions install <your-repo-uri>` is to be on the stable release branch.

Lets say you want to maintain three standard release channels, `stable`, `preview`, and `dev`. You would do all your standard development in the `dev` branch. When you are ready to do a preview release, you merge that branch into your `preview` branch. When you are ready to promote your preview branch to stable, you merge `preview` into your stable branch (which might be your default branch or a different branch).

You can also cherry pick changes from one branch into another using `git cherry-pick`, but do note that this will result in your branches having a slightly divergent history from each other, unless you force push changes to your branches on each release to restore the history to a clean slate (which may not be possible for the default branch depending on your repository settings). If you plan on doing cherry picks, you may want to avoid having your default branch be the stable branch to avoid force-pushing to the default branch which should generally be avoided.

## Releasing through Github releases

Qwen Code extensions can be distributed through [GitHub Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases). This provides a faster and more reliable initial installation experience for users, as it avoids the need to clone the repository.

Each release includes at least one archive file, which contains the full contents of the repo at the tag that it was linked to. Releases may also include [pre-built archives](#custom-pre-built-archives) if your extension requires some build step or has platform specific binaries attached to it.

When checking for updates, qwen code will just look for the latest release on github (you must mark it as such when creating the release), unless the user installed a specific release by passing `--ref=<some-release-tag>`. We do not at this time support opting in to pre-release releases or semver.

### Custom pre-built archives

Custom archives must be attached directly to the github release as assets and must be fully self-contained. This means they should include the entire extension, see [archive structure](#archive-structure).

If your extension is platform-independent, you can provide a single generic asset. In this case, there should be only one asset attached to the release.

Custom archives may also be used if you want to develop your extension within a larger repository, you can build an archive which has a different layout from the repo itself (for instance it might just be an archive of a subdirectory containing the extension).

#### Platform specific archives

To ensure Qwen Code can automatically find the correct release asset for each platform, you must follow this naming convention. The CLI will search for assets in the following order:

1.  **Platform and Architecture-Specific:** `{platform}.{arch}.{name}.{extension}`
2.  **Platform-Specific:** `{platform}.{name}.{extension}`
3.  **Generic:** If only one asset is provided, it will be used as a generic fallback.

- `{name}`: The name of your extension.
- `{platform}`: The operating system. Supported values are:
  - `darwin` (macOS)
  - `linux`
  - `win32` (Windows)
- `{arch}`: The architecture. Supported values are:
  - `x64`
  - `arm64`
- `{extension}`: The file extension of the archive (e.g., `.tar.gz` or `.zip`).

**Examples:**

- `darwin.arm64.my-tool.tar.gz` (specific to Apple Silicon Macs)
- `darwin.my-tool.tar.gz` (for all Macs)
- `linux.x64.my-tool.tar.gz`
- `win32.my-tool.zip`

#### Archive structure

Archives must be fully contained extensions and have a supported root manifest: `qwen-extension.json` for a native Qwen extension, or `plugin.json` for an [Agent Plugins v1](./agent-plugins.md) package.

The rest of the layout should look exactly the same as a typical extension, see [introduction.md](./introduction.md).

#### Example GitHub Actions workflow

Here is an example of a GitHub Actions workflow that builds and releases a Qwen Code extension for multiple platforms:

```yaml
name: Release Extension

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '22'

      - name: Install dependencies
        run: npm ci

      - name: Build extension
        run: npm run build

      - name: Create release assets
        run: |
          npm run package -- --platform=darwin --arch=arm64
          npm run package -- --platform=linux --arch=x64
          npm run package -- --platform=win32 --arch=x64

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            release/darwin.arm64.my-tool.tar.gz
            release/linux.arm64.my-tool.tar.gz
            release/win32.arm64.my-tool.zip
```

## Releasing through npm registry

You can publish Qwen Code extensions as scoped npm packages (e.g. `@your-org/my-extension`). This is a good fit when:

- Your team already uses npm for package distribution
- You need private registry support with existing auth infrastructure
- You want version resolution and access control handled by npm

### Package requirements

Your npm package must include a supported manifest at the package root: `qwen-extension.json` for a native Qwen extension, or `plugin.json` for an [Agent Plugins v1](./agent-plugins.md) package. The npm tarball is simply another delivery mechanism.

A minimal package structure looks like:

```
my-extension/
├── package.json
├── qwen-extension.json
├── QWEN.md              # optional context file
├── commands/             # optional custom commands
├── skills/               # optional custom skills
└── agents/               # optional custom subagents
```

Make sure the selected root manifest and all referenced package files are included in your published package (i.e. not excluded by `.npmignore` or the `files` field in `package.json`).

### Publishing

Use standard npm publishing tools:

```bash
# Publish to the default registry
npm publish

# Publish to a private/custom registry
npm publish --registry https://your-registry.com
```

### Installation

Users install your extension using the scoped package name:

```bash
# Install latest version
qwen extensions install @your-org/my-extension

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

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

### Update behavior

- Extensions installed without a version pin (e.g. `@scope/pkg`) track the `latest` dist-tag.
- Extensions installed with a dist-tag (e.g. `@scope/pkg@beta`) track that specific tag.
- Extensions pinned to an exact version (e.g. `@scope/pkg@1.2.0`) are always considered up-to-date and will not prompt for updates.

### Authentication for private registries

Qwen Code reads npm auth credentials automatically:

1. **`NPM_TOKEN` environment variable** — highest priority
2. **`.npmrc` file** — supports both host-level and path-scoped `_authToken` entries (e.g. `//your-registry.com/:_authToken=TOKEN` or `//pkgs.dev.azure.com/org/_packaging/feed/npm/registry/:_authToken=TOKEN`)

`.npmrc` files are read from the current directory and the user's home directory.

### Managing release channels

You can use npm dist-tags to manage release channels:

```bash
# Publish a beta release
npm publish --tag beta

# Users install beta channel
qwen extensions install @your-org/my-extension@beta
```

This works similarly to git branch-based release channels but uses npm's native dist-tag mechanism.

---

### Users Extension Getting Started Extensions (`docs/users/extension/getting-started-extensions.md`)

# Getting Started with Qwen Code Extensions

This guide will walk you through creating your first Qwen Code extension. You'll learn how to set up a new extension, add a custom tool via an MCP server, create a custom command, and provide context to the model with a `QWEN.md` file.

## Prerequisites

Before you start, make sure you have the Qwen Code installed and a basic understanding of Node.js and TypeScript.

## Step 1: Create a New Extension

The easiest way to start is by using one of the built-in templates. We'll use the `mcp-server` example as our foundation.

Run the following command to create a new directory called `my-first-extension` with the template files:

```bash
qwen extensions new my-first-extension mcp-server
```

This will create a new directory with the following structure:

```
my-first-extension/
├── example.ts
├── qwen-extension.json
├── package.json
└── tsconfig.json
```

## Step 2: Understand the Extension Files

Let's look at the key files in your new extension.

### `qwen-extension.json`

This is the manifest file for your extension. It tells Qwen Code how to load and use your extension.

```json
{
  "name": "my-first-extension",
  "version": "1.0.0",
  "mcpServers": {
    "nodeServer": {
      "command": "node",
      "args": ["${extensionPath}${/}dist${/}example.js"],
      "cwd": "${extensionPath}"
    }
  }
}
```

- `name`: The unique name for your extension.
- `version`: The version of your extension.
- `mcpServers`: This section defines one or more Model Context Protocol (MCP) servers. MCP servers are how you can add new tools for the model to use.
  - `command`, `args`, `cwd`: These fields specify how to start your server. Notice the use of the `${extensionPath}` variable, which Qwen Code replaces with the absolute path to your extension's installation directory. This allows your extension to work regardless of where it's installed.

### `example.ts`

This file contains the source code for your MCP server. It's a simple Node.js server that uses the `@modelcontextprotocol/sdk`.

```typescript
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
  name: 'prompt-server',
  version: '1.0.0',
});

// Registers a new tool named 'fetch_posts'
server.registerTool(
  'fetch_posts',
  {
    description: 'Fetches a list of posts from a public API.',
    inputSchema: z.object({}).shape,
  },
  async () => {
    const apiResponse = await fetch(
      'https://jsonplaceholder.typicode.com/posts',
    );
    const posts = await apiResponse.json();
    const response = { posts: posts.slice(0, 5) };
    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(response),
        },
      ],
    };
  },
);

// ... (prompt registration omitted for brevity)

const transport = new StdioServerTransport();
await server.connect(transport);
```

This server defines a single tool called `fetch_posts` that fetches data from a public API.

### `package.json` and `tsconfig.json`

These are standard configuration files for a TypeScript project. The `package.json` file defines dependencies and a `build` script, and `tsconfig.json` configures the TypeScript compiler.

## Step 3: Build and Link Your Extension

Before you can use the extension, you need to compile the TypeScript code and link the extension to your Qwen Code installation for local development.

1.  **Install dependencies:**

    ```bash
    cd my-first-extension
    npm install
    ```

2.  **Build the server:**

    ```bash
    npm run build
    ```

    This will compile `example.ts` into `dist/example.js`, which is the file referenced in your `qwen-extension.json`.

3.  **Link the extension:**

    The `link` command creates a symbolic link from the Qwen Code extensions directory to your development directory. This means any changes you make will be reflected immediately without needing to reinstall.

    ```bash
    qwen extensions link .
    ```

Now, restart your Qwen Code session. The new `fetch_posts` tool will be available. You can test it by asking: "fetch posts".

## Step 4: Add a Custom Command

Custom commands provide a way to create shortcuts for complex prompts. Let's add a command that searches for a pattern in your code.

1.  Create a `commands` directory and a subdirectory for your command group:

    ```bash
    mkdir -p commands/fs
    ```

2.  Create a file named `commands/fs/grep-code.md`:

    ```markdown
    ---
    description: Search for a pattern in code and summarize findings
    ---

    Please summarize the findings for the pattern `{{args}}`.

    Search Results:
    !{grep -r {{args}} .}
    ```

    This command, `/fs:grep-code`, will take an argument, run the `grep` shell command with it, and pipe the results into a prompt for summarization.

> **Note:** Commands use Markdown format with optional YAML frontmatter. TOML format is deprecated but still supported for backwards compatibility.

After saving the file, restart the Qwen Code. You can now run `/fs:grep-code "some pattern"` to use your new command.

## Step 5: Add Custom Skills and Subagents (Optional)

Extensions can also provide custom skills and subagents to extend Qwen Code's capabilities.

### Adding a Custom Skill

Skills are model-invoked capabilities that the AI can automatically use when relevant.

1.  Create a `skills` directory with a skill subdirectory:

    ```bash
    mkdir -p skills/code-analyzer
    ```

2.  Create a `skills/code-analyzer/SKILL.md` file:

    ```markdown
    ---
    name: code-analyzer
    description: Analyzes code structure and provides insights about complexity, dependencies, and potential improvements
    ---

    # Code Analyzer

    ## Instructions

    When analyzing code, focus on:

    - Code complexity and maintainability
    - Dependencies and coupling
    - Potential performance issues
    - Suggestions for improvements

    ## Examples

    - "Analyze the complexity of this function"
    - "What are the dependencies of this module?"
    ```

### Adding a Custom Subagent

Subagents are specialized AI assistants for specific tasks.

1.  Create an `agents` directory:

    ```bash
    mkdir -p agents
    ```

2.  Create an `agents/refactoring-expert.md` file:

    ```markdown
    ---
    name: refactoring-expert
    description: Specialized in code refactoring, improving code structure and maintainability
    tools:
      - read_file
      - write_file
      - read_many_files
    ---

    You are a refactoring specialist focused on improving code quality.

    Your expertise includes:

    - Identifying code smells and anti-patterns
    - Applying SOLID principles
    - Improving code readability and maintainability
    - Safe refactoring with minimal risk

    For each refactoring task:

    1. Analyze the current code structure
    2. Identify areas for improvement
    3. Propose refactoring steps
    4. Implement changes incrementally
    5. Verify functionality is preserved
    ```

After restarting Qwen Code, your custom skills will be available via `/skills` and subagents via `/agents manage`.

An extension skill is registered under its owner's name: the skill above becomes `my-first-extension:code-analyzer`, so you run it as `/my-first-extension:code-analyzer`. `skills.disabled` can block it under either that name or the plain `code-analyzer` you authored; `skills.enabled` opts it back in under the prefixed name only. See [Extension Skills](../features/skills.md#extension-skills).

### Adding Conditional Rules

An extension can also ship a `rules/` directory. A rule is a Markdown file whose `paths:` frontmatter says which files it applies to, and it enters the prompt only when a tool call touches a matching file:

```markdown
---
description: How this project charts data
paths:
  - 'src/**/*.chart.ts'
---

Use the palette from `theme/charts.ts`. Never hard-code a hex value.
```

**An extension's rules must be conditional** — a rule with no `paths:` is skipped and named in a startup warning. A baseline rule would be part of every request, which is exactly what the `QWEN.md` note in the next step asks you to avoid. Rules appear in the prompt labelled by owner, as `my-first-extension:rules/charting.md`. See [Rules](../features/rules.md).

## Step 6: Add a Custom `QWEN.md`

You can provide persistent context to the model by adding a `QWEN.md` file to your extension. This is useful for giving the model instructions on how to behave or information about your extension's tools. Note that you may not always need this for extensions built to expose commands and prompts.

> **This is the most expensive place to put instructions.** An extension's context file is concatenated into the system prompt of **every request of every session the extension is active in**, whether or not the work at hand has anything to do with your extension — there is no relevance gating and no size limit. In one measured session, nine extensions' context files accounted for 9,989 tokens, 21% of everything the request carried before the conversation itself. Keep the context file to the few facts that are always true — the extension's identity, its vocabulary, a hard constraint — and put scenario guidance in a [skill](../features/skills.md) instead. A skill is listed by its name and description only — in the same measured session, 84 skills averaged about 55 tokens each — and loads its body when invoked, and a skill [gated on `paths:`](../features/skills.md#optional-gate-a-skill-on-file-paths-paths) is not even listed until a matching file is touched. `/context detail` names each extension's context file so you can see what yours costs.

1.  Create a file named `QWEN.md` in the root of your extension directory:

    ```markdown
    # My First Extension Instructions

    You are an expert developer assistant. When the user asks you to fetch posts, use the `fetch_posts` tool. Be concise in your responses.
    ```

2.  Update your `qwen-extension.json` to tell the CLI to load this file:

    ```json
    {
      "name": "my-first-extension",
      "version": "1.0.0",
      "contextFileName": "QWEN.md",
      "mcpServers": {
        "nodeServer": {
          "command": "node",
          "args": ["${extensionPath}${/}dist${/}example.js"],
          "cwd": "${extensionPath}"
        }
      }
    }
    ```

Restart the CLI again. The model will now have the context from your `QWEN.md` file in every session where the extension is active — and in every request of those sessions, which is why the note above asks you to keep it short and to reach for a skill for anything scenario-specific.

## Step 7: Releasing Your Extension

Once you are happy with your extension, you can share it with others. The two primary ways of releasing extensions are via a Git repository or through GitHub Releases. Using a public Git repository is the simplest method.

For detailed instructions on both methods, please refer to the [Extension Releasing Guide](extension-releasing.md).

## Conclusion

You've successfully created a Qwen Code extension! You learned how to:

- Bootstrap a new extension from a template.
- Add custom tools with an MCP server.
- Create convenient custom commands.
- Add custom skills and subagents.
- Provide persistent context to the model.
- Link your extension for local development.

From here, you can explore more advanced features and build powerful new capabilities into the Qwen Code.

---

### Users Extension Introduction (`docs/users/extension/introduction.md`)

# 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](https://geminicli.com/extensions/), [Claude Code Marketplace](https://claudemarketplaces.com/), Qoder, and the portable [Agent Plugins v1](./agent-plugins.md) 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](https://claudemarketplaces.com/). Install from a marketplace and choose a plugin:

```bash
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:

```bash
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](https://claudemarketplaces.com/plugins/f-prompts.chat) marketplace:

```bash
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:

```bash
# 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](https://geminicli.com/extensions/). Simply install them using the git URL:

```bash
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](https://docs.qoder.com/en/cli/sdk/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:

```bash
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:

```bash
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](./agent-plugins.md) 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.

```bash
# 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](./extension-releasing.md#releasing-through-npm-registry) 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 submodules or Git LFS, and it caps downloads at 100 MiB compressed and archives at 100,000 entries / 1 GiB expanded / 8 MiB path metadata, including files materialized from at most 100 symlinks. Symlinks directly targeting regular files in the repository are supported on systems that permit symlink creation; Windows may require Developer Mode or elevated privileges. The fallback rejects directory, chained, dangling, absolute, escaping, hard, and POSIX literal-backslash-target links. Other Agent Plugin install paths continue to omit symlinks. Release-based installs are still preferred when a repository publishes releases.

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

This will install the github mcp server extension.

#### From Local Path

```bash
qwen extensions install /path/to/your/extension
```

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

```bash
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

```bash
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:

```bash
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:

```bash
# 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](./agent-plugins.md).

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:

```json
{
  "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",
  "workflows": "workflows",
  "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](../configuration/settings.md). 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](../features/channels/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.
- `workflows`: A directory, or a list of directories and `.js` files, containing workflow scripts (default: `workflows`). See [Custom workflows](#custom-workflows).
- `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:**

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

**List all settings and current values for an extension:**

```bash
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](../features/commands.md#4-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**

An extension named `gcp` with the following structure:

```
.qwen/extensions/gcp/
├── qwen-extension.json
└── skills/
    └── pdf-processor/
        └── SKILL.md   # frontmatter: name: pdf-processor
```

provides one skill, registered as `gcp:pdf-processor` — the extension's `name`, a colon, then the name the `SKILL.md` authors. Run it with `/gcp:pdf-processor`; `/skills` lists it and labels it with the extension's display name, falling back to its `name` when the manifest declares none.

Unlike the extension's custom commands, which are named after their files (`/deploy` and `/gcs:sync` above, and prefixed only when one collides — see Conflict resolution below), an extension skill always carries its owner: two extensions that both ship a `pdf-processor` give you two skills instead of one shadowing the other. The prefix is added as the skill loads, so the `name` in your `SKILL.md` is never rewritten on disk.

Settings that name skills treat the two spellings asymmetrically: `skills.disabled` blocks a skill under either name, while `skills.enabled` opts it in under the prefixed name only. See [Extension Skills](../features/skills.md#extension-skills).

### 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.

### Custom workflows

Extensions can ship workflow scripts by placing `.js` files in a `workflows/` subdirectory, or in the directories and files the manifest lists in `workflows`. They appear only when Workflows are enabled with the [`tools.workflowsEnabled`](../configuration/settings.md) setting, which is off by default; the install consent prompt lists them either way.

**Example**

An extension named `gcp` with the following structure:

```
.qwen/extensions/gcp/
├── qwen-extension.json
└── workflows/
    └── deep-research.js
```

provides one workflow when its script declares a static `meta` object with `name: 'deep-research'`, registered as `gcp:deep-research` — the extension's `name`, a colon, then `meta.name`. Run it with `/gcp:deep-research`, call it from another workflow with `workflow('gcp:deep-research')`, or let the model run it by name with `Workflow({ name: 'gcp:deep-research' })`. Like an extension skill, an extension workflow always carries its owner, so it never shadows one of your project or user workflows. If the same extension also ships a skill with that name, the skill keeps `/gcp:deep-research` and the workflow's slash command is renamed to `/gcp.gcp:deep-research`, as for any colliding extension command; a `slashCommands.disabled` entry written as `gcp:deep-research` still removes both. A user or project custom command with the same name (for example `commands/gcp/deep-research.md`) loads last and takes the slash command, and the workflow then stays reachable through `workflow('gcp:deep-research')`.

Each script must declare a static `export const meta = { name, description }` block. The `description` is shown in the install consent prompt and in the command list. The file name may differ from `meta.name`; calls always use the metadata name. If multiple scripts declare the same `meta.name`, the first discovered script is kept. A `description` longer than 500 characters is shortened wherever it is shown.

A script can also declare `whenToUse`, a sentence saying when the workflow applies:

```js
export const meta = {
  name: 'deep-research',
  description: 'Researches a question across the codebase and the web',
  whenToUse:
    'When the user asks for a sourced, multi-angle answer to an open question',
};
```

Only a workflow that declares `whenToUse` is listed for the model, together with its description, so the model can start it when a request matches; each run still goes through the workflow approval. Without it, the model does not see the workflow, which then runs when you invoke it, ask for it by name, or another workflow calls it. `whenToUse` is shortened past 500 characters, like `description`, and since it lives in the script, changing it makes the next update ask for consent again.

In the interactive UI, `/gcp:deep-research` starts the workflow directly. In headless mode and over ACP, the same command asks the model to run it by name, and the approval follows.

To let the model start your extension's workflows without letting it write and run scripts of its own, deploy with [`tools.workflowNameOnly`](../configuration/settings.md) (or `QWEN_CODE_WORKFLOW_NAME_ONLY=1`) and allow the workflows by name, for example `Workflow(name:gcp:deep-research)`. The lock makes every run the model starts addressable by such a rule; it does not approve anything by itself, so keep asking for names you have not allowed.

Discovery is deliberately narrow:

- Only `.js` files directly inside each directory are read; subdirectories are ignored.
- `meta.name` must use lower-case letters, digits, and hyphens, start with a letter, and contain at most 41 characters.
- Every declared path must stay inside the extension directory. A linked extension (`qwen extensions link`) skips symlinked workflow files and directories; an installed extension is a copy in which each symlink has already been replaced by the file it points to.
- Scripts larger than 256 KiB, or without a valid `meta` block, are skipped with a warning.

Installing an extension lists its workflows in the consent prompt. An update asks again when it adds or removes a workflow, changes a workflow's name or description, or changes a script's code; when only code changed, the prompt names the changed scripts. Extension workflows follow the same rules as your own saved workflows: they are hidden in untrusted folders and in bare mode, and each run goes through the usual workflow approval, which shows the start of the script. An "always allow" for a workflow run by name or path is saved as a rule pinned to the script's content, such as `Workflow(name:gcp:deep-research,sha256:3f2a9c1d0b4e5f67)`, so it stops applying once the script changes. A rule you write without `sha256`, such as `Workflow(name:gcp:deep-research)`, allows every version of the script.

Edits to files in the default `workflows/` directory are picked up automatically. Changes under other declared paths take effect after `/reload-plugins` or a restart.

Claude Code plugins that ship workflows are converted on install. A plugin that declares no `workflows` keeps its `workflows/` directory, which is discovered as the default. When the plugin declares `workflows`, the declared files retain their relative paths and are listed explicitly in the converted extension's manifest, and only those files are discovered: files with the same basename in different directories stay distinct under their `meta.name` values, and a `workflows/` directory the plugin also ships is copied but not read. A symlink inside a declared directory is copied as a regular file when its target stays inside the plugin.

### 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).                                                                                                                          |

---

### Users Features Approval Mode (`docs/users/features/approval-mode.md`)

# Approval Mode

Qwen Code offers five distinct permission modes that allow you to flexibly control how AI interacts with your code and system based on task complexity and risk level.

## Permission Modes Comparison

| Mode                 | File Editing                | Shell Commands              | Best For                                                                                               | Risk Level |
| -------------------- | --------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ | ---------- |
| **Plan**​            | ❌ Read-only analysis only  | ❌ Not executed             | • Code exploration <br>• Planning complex changes <br>• Safe code review                               | Lowest     |
| **Ask Permissions**​ | ✅ Manual approval required | ✅ Manual approval required | • New/unfamiliar codebases <br>• Critical systems <br>• Team collaboration <br>• Learning and teaching | Low        |
| **Auto-Edit**​       | ✅ Auto-approved            | ❌ Manual approval required | • Daily development tasks <br>• Refactoring and code improvements <br>• Safe automation                | Medium     |
| **Auto**​            | ✅ Classifier-evaluated     | ✅ Classifier-evaluated     | • Long autonomous sessions <br>• When Auto-Edit is too cautious but YOLO is too risky                  | Medium     |
| **YOLO**​            | ✅ Auto-approved            | ✅ Auto-approved            | • Trusted personal projects <br>• Automated scripts/CI/CD <br>• Batch processing tasks                 | Highest    |

> [!NOTE]
>
> The mode previously named **Default** has been renamed to **Ask Permissions** to better describe its behavior. The underlying configuration value (`tools.approvalMode: "default"`) and the `/approval-mode default` command are unchanged for backward compatibility.

### Quick Reference Guide

- **Start in Plan Mode**: Great for understanding before making changes
- **Auto Mode (default)**: The default out-of-the-box experience — an LLM classifier auto-approves safe actions and blocks risky ones, minimizing interruptions while keeping a safety net
- **Switch to Ask Permissions**: When you want manual approval for every file edit and shell command
- **Switch to Auto-Edit**: When you're making lots of safe code changes
- **Use YOLO sparingly**: Only for trusted automation in controlled environments

> [!tip]
>
> You can quickly cycle through modes during a session using **Shift+Tab** (or **Tab** on Windows). The terminal status bar shows your current mode, so you always know what permissions Qwen Code has.

> The cycle order is: **plan → default → auto-edit → auto → yolo → plan → ...**

## 1. Use Plan Mode for safe code analysis

Plan Mode instructs Qwen Code to create a plan by analyzing the codebase with **read-only** operations, perfect for exploring codebases, planning complex changes, or reviewing code safely.

### When to use Plan Mode

- **Multi-step implementation**: When your feature requires making edits to many files
- **Code exploration**: When you want to research the codebase thoroughly before changing anything
- **Interactive development**: When you want to iterate on the direction with Qwen Code

### How to use Plan Mode

**Turn on Plan Mode during a session**

You can switch into Plan Mode during a session using **Shift+Tab** (or **Tab** on Windows) to cycle through permission modes.

If you are in Normal Mode, **Shift+Tab** (or **Tab** on Windows) first switches into `auto-edits` Mode, indicated by `⏵⏵ accept edits on` at the bottom of the terminal. A subsequent **Shift+Tab** (or **Tab** on Windows) will switch into Plan Mode, indicated by `⏸ plan mode`.

**Use the `/plan` command**

The `/plan` command provides a quick shortcut for entering and exiting Plan Mode:

Regular planning requests do not switch modes by themselves. If you want the read-only Plan Mode workflow, use `/plan`, the keyboard shortcut, or set the approval mode to `plan` explicitly.

```bash
/plan                          # Enter plan mode
/plan refactor the auth module # Enter plan mode and start planning
/plan exit                     # Exit plan mode, restore previous mode
```

When you exit Plan Mode with `/plan exit`, your previous approval mode is automatically restored (e.g., if you were in Auto-Edit before entering Plan Mode, you'll return to Auto-Edit).

**Start a new session in Plan Mode**

To start a new session in Plan Mode, use the `/approval-mode` then select `plan`

```bash
/approval-mode
```

**Run "headless" queries in Plan Mode**

You can also run a query in Plan Mode directly with `-p` or `prompt`:

```bash
qwen --prompt "What is machine learning?"
```

### Example: Planning a complex refactor

```bash
/plan I need to refactor our authentication system to use OAuth2. Create a detailed migration plan.
```

Qwen Code enters Plan Mode and analyzes the current implementation to create a comprehensive plan. Refine with follow-ups:

```
What about backward compatibility?
How should we handle database migration?
```

### Configure Plan Mode as default

```json
// .qwen/settings.json
{
  "tools": {
    "approvalMode": "plan"
  }
}
```

## 2. Use Ask Permissions Mode for Controlled Interaction

Ask Permissions Mode is the standard way to work with Qwen Code. In this mode, you maintain full control over all potentially risky operations - Qwen Code will ask for your approval before making any file changes or executing shell commands.

### When to use Ask Permissions Mode

- **New to a codebase**: When you're exploring an unfamiliar project and want to be extra cautious
- **Critical systems**: When working on production code, infrastructure, or sensitive data
- **Learning and teaching**: When you want to understand each step Qwen Code is taking
- **Team collaboration**: When multiple people are working on the same codebase
- **Complex operations**: When the changes involve multiple files or complex logic

### How to use Ask Permissions Mode

**Turn on Ask Permissions Mode during a session**

You can switch into Ask Permissions Mode during a session using **Shift+Tab**​ (or **Tab** on Windows) to cycle through permission modes. If you're in any other mode, pressing **Shift+Tab** (or **Tab** on Windows) will eventually cycle back to Ask Permissions Mode, indicated by the absence of any mode indicator at the bottom of the terminal.

**Start a new session in Ask Permissions Mode**

Ask Permissions Mode is the initial mode when you start Qwen Code. If you've changed modes and want to return to Ask Permissions Mode, use:

```
/approval-mode default
```

**Run "headless" queries in Ask Permissions Mode**

When running headless commands, Ask Permissions Mode is the default behavior. You can explicitly specify it with:

```
qwen --prompt "Analyze this code for potential bugs"
```

### Example: Safely implementing a feature

```
/approval-mode default
```

```
I need to add user profile pictures to our application. The pictures should be stored in an S3 bucket and the URLs saved in the database.
```

Qwen Code will analyze your codebase and propose a plan. It will then ask for approval before:

1. Creating new files (controllers, models, migrations)
2. Modifying existing files (adding new columns, updating APIs)
3. Running any shell commands (database migrations, dependency installation)

You can review each proposed change and approve or reject it individually.

### Configure Ask Permissions Mode as default

```bash
// .qwen/settings.json
{
  "tools": {
    "approvalMode": "default"
  }
}
```

## 3. Auto Edits Mode

Auto-Edit Mode instructs Qwen Code to automatically approve file edits while requiring manual approval for shell commands, ideal for accelerating development workflows while maintaining system safety.

Auto-approved edit tools include `edit`, `write_file`, and `notebook_edit`.

### When to use Auto-Accept Edits Mode

- **Daily development**: Ideal for most coding tasks
- **Safe automation**: Allows AI to modify code while preventing accidental execution of dangerous commands
- **Team collaboration**: Use in shared projects to avoid unintended impacts on others

### How to switch to this mode

```
# Switch via command
/approval-mode auto-edit

# Or use keyboard shortcut
Shift+Tab (or Tab on Windows) # Switch from other modes
```

### Workflow Example

1. You ask Qwen Code to refactor a function
2. AI analyzes the code and proposes changes
3. **Automatically**​ applies all file changes without confirmation
4. If tests need to be run, it will **request approval**​ to execute `npm test`

## 4. Auto Mode - Classifier-Driven Approval

Auto Mode sits between Auto-Edit and YOLO. An LLM classifier evaluates each
shell command, network call, and out-of-workspace edit and auto-approves
the ones it judges safe while blocking risky ones. Most read-only operations
and in-workspace edits skip the classifier for speed.

See [auto-mode.md](./auto-mode.md) for the full reference (hints
configuration, troubleshooting, FAQ).

### When to use Auto Mode

- **Long autonomous sessions**: When Ask Permissions Mode interrupts too often but
  YOLO is too risky.
- **Trusted projects**: Internal codebases where the agent should keep
  moving but you still want a guardrail on destructive shell commands and
  outbound network calls.
- **Headless / scheduled runs**: Where Auto-Edit isn't enough (the agent
  needs to run shell commands too) but you want safety on `rm -rf /`,
  `curl ... | sh`, credential exfiltration, etc.

### How to use Auto Mode

**Turn on Auto Mode during a session**

Press **Shift+Tab** (or **Tab** on Windows) to cycle into Auto Mode. The
status bar shows the active mode.

**Use the `/approval-mode` command**

```
/approval-mode auto
```

The first time you enter Auto Mode, an information message explains how it
works. The notice does not appear again.

**Start a new session in Auto Mode**

```jsonc
// .qwen/settings.json
{
  "tools": {
    "approvalMode": "auto",
  },
}
```

### What Auto Mode auto-approves vs blocks

The classifier is biased toward blocking when uncertain. Defaults:

- **Auto-approved**: read-only commands (ls, cat, git status, grep, find),
  package install in cwd, build/test commands, file edits inside the
  workspace, local-only operations.
- **Blocked**: irreversible destruction (rm -rf /, fdisk, mkfs),
  code-from-external execution (curl | sh, eval of remote content),
  credential exfiltration, unauthorized persistence (.bashrc edits,
  crontab), security weakening, force-push to main/master.

You can customize the classifier's judgement via natural-language hints in
settings.json. See [auto-mode.md](./auto-mode.md#configuring-hints).

### Safety guardrails

- **Hard rules remain in force**: `permissions.deny` rules block actions
  before the classifier ever runs.
- **Over-broad allow rules are stripped while in Auto Mode**: e.g.
  `permissions.allow: ["Bash"]` (allow every shell command) defeats the
  classifier; entering Auto Mode temporarily disables such rules so the
  classifier can do its job. The rules are restored when you leave Auto
  Mode. Settings on disk are never modified.
- **Fail-closed**: when the classifier API is unreachable, the action is
  blocked rather than allowed. After two consecutive unavailable calls,
  the next tool call falls back to manual approval.
- **Loop guard**: after three consecutive blocks — classifier policy blocks
  and deterministic destructive-command blocks both count — the call that
  reaches the third block also falls back to manual approval so the agent
  isn't stuck cycling on a dead-end approach.

### Example

```
/approval-mode auto
Refactor the auth module to use OAuth2. Run the full test suite afterwards.
```

Qwen Code makes the file edits (in-workspace edits skip the classifier),
runs `npm test` (classifier judges safe), and surfaces a block if it ever
tries something risky like `rm -rf /Users/me/.aws`. You can review the
reason inline and decide whether to switch to Ask Permissions Mode for that step.

### Configure Auto Mode as default

```jsonc
// .qwen/settings.json
{
  "tools": {
    "approvalMode": "auto",
  },
  "permissions": {
    "autoMode": {
      "hints": {
        "allow": ["Running pytest, mypy, and ruff on this Python repo"],
        "deny": ["Any network call to intranet.example.com"],
      },
      "environment": ["Open-source monorepo; commits are signed"],
      // Optional: route ALL shell commands (including read-only ones like
      // ls, cat) through the classifier for defense-in-depth.
      // "classifyAllShell": true,
      // Optional: send MCP tool calls to the classifier by name only
      // (arguments are forwarded by default).
      // "mcp": { "forwardArguments": false },
    },
  },
}
```

## 5. YOLO Mode - Full Automation

YOLO Mode grants Qwen Code the highest permissions, automatically approving all tool calls including file editing and shell commands.

### When to use YOLO Mode

- **Automated scripts**: Running predefined automated tasks
- **CI/CD pipelines**: Automated execution in controlled environments
- **Personal projects**: Rapid iteration in fully trusted environments
- **Batch processing**: Tasks requiring multi-step command chains

> [!warning]
>
> **Use YOLO Mode with caution**: AI can execute any command with your terminal permissions. Ensure:
>
> 1. You trust the current codebase
> 2. You understand all actions AI will perform
> 3. Important files are backed up or committed to version control

### How to enable YOLO Mode

```
# Temporarily enable (current session only)
/approval-mode yolo

# Set as project default
/approval-mode yolo --project

# Set as user global default
/approval-mode yolo --user
```

### Configuration Example

```bash
// .qwen/settings.json
{
  "tools": {
    "approvalMode": "yolo"
  }
}
```

### Automated Workflow Example

```bash
# Fully automated refactoring task
qwen --prompt "Run the test suite, fix all failing tests, then commit changes"

# Without human intervention, AI will:
# 1. Run test commands (auto-approved)
# 2. Fix failed test cases (auto-edit files)
# 3. Execute git commit (auto-approved)
```

## Mode Switching & Configuration

### Keyboard Shortcut Switching

During a Qwen Code session, use **Shift+Tab**​ (or **Tab** on Windows) to quickly cycle through the five modes:

```
Plan Mode → Ask Permissions Mode → Auto-Edit Mode → Auto Mode → YOLO Mode → Plan Mode
```

### Persistent Configuration

```
// Project-level: ./.qwen/settings.json
// User-level: ~/.qwen/settings.json
{
  "tools": {
    "approvalMode": "auto-edit"  // or "plan", "default", "auto", "yolo"
  }
}
```

### Mode Usage Recommendations

1. **New to codebase**: Start with **Plan Mode**​ for safe exploration
2. **Daily development tasks**: Use **Auto-Accept Edits**​ (default mode), efficient and safe
3. **Automated scripts**: Use **YOLO Mode**​ in controlled environments for full automation
4. **Complex refactoring**: Use **Plan Mode**​ first for detailed planning, then switch to appropriate mode for execution

---

### Users Features Arena (`docs/users/features/arena.md`)

# Agent Arena

> Dispatch multiple AI models simultaneously to execute the same task, compare their solutions side-by-side, and select the best result to apply to your workspace.

> [!warning]
> Agent Arena is experimental. It has [known limitations](#limitations) around display modes and session management.

Agent Arena lets you pit multiple AI models against each other on the same task. Each model runs as a fully independent agent in its own isolated Git worktree, so file operations never interfere. When all agents finish, you compare results and select a winner to merge back into your main workspace.

Unlike [subagents](./sub-agents.md), which delegate focused subtasks within a single session, Arena agents are complete, top-level agent instances — each with its own model, context window, and full tool access.

This page covers:

- [When to use Agent Arena](#when-to-use-agent-arena)
- [Starting an arena session](#start-an-arena-session)
- [Interacting with agents](#interact-with-agents), including display modes and navigation
- [Comparing results and selecting a winner](#compare-results-and-select-a-winner)
- [Best practices](#best-practices)

## When to use Agent Arena

Agent Arena is most effective when you want to **evaluate or compare** how different models tackle the same problem. The strongest use cases are:

- **Model benchmarking**: Evaluate different models' capabilities on real tasks in your actual codebase, not synthetic benchmarks
- **Best-of-N selection**: Get multiple independent solutions and pick the best implementation
- **Exploring approaches**: See how different models reason about and solve the same problem — useful for learning and insight
- **Risk reduction**: For critical changes, validate that multiple models converge on a similar approach before committing

Agent Arena uses significantly more tokens than a single session (each agent has its own context window and model calls). It works best when the value of comparison justifies the cost. For routine tasks where you trust your default model, a single session is more efficient.

## Start an arena session

Use the `/arena` slash command to launch a session. Specify the models you want to compete and the task:

```
/arena --models qwen3.5-plus,glm-5,kimi-k2.5 "Refactor the authentication module to use JWT tokens"
```

If you omit `--models`, an interactive model selection dialog appears, letting you pick from your configured providers.

### What happens when you start

1. **Worktree setup**: Qwen Code creates isolated Git worktrees for each agent at `~/.qwen/arena/<session-id>/worktrees/<model-name>/`. Each worktree mirrors your current working directory state exactly — including staged changes, unstaged changes, and untracked files.
2. **Agent spawning**: Each agent starts in its own worktree with full tool access and its configured model. Agents are launched sequentially but execute in parallel.
3. **Execution**: All agents work on the task independently with no shared state or communication. You can monitor their progress and interact with any of them.
4. **Completion**: When all agents finish (or fail), you enter the result comparison phase.

## Interact with agents

### Display modes

Agent Arena currently supports **in-process mode**, where all agents run asynchronously within the same terminal process. A tab bar at the bottom of the terminal lets you switch between agents.

> [!note]
> **Split-pane display modes are planned for the future.** We intend to support tmux-based and iTerm2-based split-pane layouts, where each agent gets its own terminal pane for true side-by-side viewing. Currently, only in-process tab switching is available.

### Navigate between agents

In in-process mode, use keyboard shortcuts to switch between agent views:

| Shortcut | Action                            |
| :------- | :-------------------------------- |
| `Right`  | Switch to the next agent tab      |
| `Left`   | Switch to the previous agent tab  |
| `Up`     | Switch focus to the input box     |
| `Down`   | Switch focus to the agent tab bar |

The tab bar shows each agent's current status:

| Indicator | Meaning                |
| :-------- | :--------------------- |
| `●`       | Running or idle        |
| `✓`       | Completed successfully |
| `✗`       | Failed                 |
| `○`       | Cancelled              |

### Interact with individual agents

When viewing an agent's tab, you can:

- **Send messages** — type in the input area to give the agent additional instructions
- **Approve tool calls** — if an agent requests tool approval, the confirmation dialog appears in its tab
- **View full history** — scroll through the agent's complete conversation, including model output, tool calls, and results

Each agent is a full, independent session. Anything you can do with the main agent, you can do with an arena agent.

## Compare results and select a winner

When all agents complete, the Arena enters the result comparison phase. You'll see:

- **Status summary**: Which agents succeeded, failed, or were cancelled
- **Execution metrics**: Duration, rounds of reasoning, token usage, and tool call counts for each agent
- **Arena comparison summary**: Files changed in common vs. by one agent only, line-change counts, token efficiency, and a high-level approach summary generated from each agent's diff, metrics, and conversation history

A selection dialog presents the successful agents. Choose one to apply its changes to your main workspace, or discard all results. Press `p` to toggle a quick preview for the highlighted agent, or `d` to toggle that agent's detailed diff before selecting a winner.

### What happens when you select a winner

1. The winning agent's changes are extracted as a diff against the baseline
2. The diff is applied to your main working directory
3. All worktrees and temporary branches are cleaned up automatically

If you want to inspect the complete reasoning path before deciding, each agent's full conversation history is still available via the tab bar while the selection dialog is active.

## Configuration

Arena settings are nested under `agents.arena` in
[settings.json](../configuration/settings.md). The schema accepts
`maxRoundsPerAgent` and `timeoutSeconds`, but the CLI currently drops both
values when building the configuration used by `/arena`. Setting either field
therefore does not limit `/arena` runs.

| Setting                          | Description                                                                                          | Default                         |
| :------------------------------- | :--------------------------------------------------------------------------------------------------- | :------------------------------ |
| `agents.arena.worktreeBaseDir`   | Custom base directory for Arena worktrees. Use an absolute path; `~` is not expanded.                | The Qwen home `arena` directory |
| `agents.arena.maxRoundsPerAgent` | Accepted by the schema, but currently not forwarded to `/arena`; setting it has no effect on the run | Unset                           |
| `agents.arena.timeoutSeconds`    | Accepted by the schema, but currently not forwarded to `/arena`; setting it has no effect on the run | Unset                           |

## Best practices

### Choose models that complement each other

Arena is most valuable when you compare models with meaningfully different strengths. For example:

```
/arena --models qwen3.5-plus,glm-5,kimi-k2.5 "Optimize the database query layer"
```

Comparing three versions of the same model family yields less insight than comparing across providers.

### Keep tasks self-contained

Arena agents work independently with no communication. Tasks should be fully describable in the prompt without requiring back-and-forth:

**Good**: "Refactor the payment module to use the strategy pattern. Update all tests."

**Less effective**: "Let's discuss how to improve the payment module" — this benefits from conversation, which is better suited to a single session.

### Limit the number of agents

Up to 5 agents can run simultaneously. In practice, 2-3 agents provide the best balance of comparison value to resource cost. More agents means:

- Higher token costs (each agent has its own context window)
- Longer total execution time
- More results to compare

Start with 2-3 and scale up only when the comparison value justifies it.

### Use Arena for high-impact decisions

Arena shines when the stakes justify running multiple models:

- Choosing an architecture for a new module
- Selecting an approach for a complex refactor
- Validating a critical bug fix from multiple angles

For routine changes like renaming a variable or updating a config file, a single session is faster and cheaper.

## Troubleshooting

### Agents failing to start

- Verify that each model in `--models` is properly configured with valid API credentials
- Check that your working directory is a Git repository (worktrees require Git)
- Ensure you have write access to the `arena` directory under the Qwen home
  directory (the default worktree base directory)

### Worktree creation fails

- Run `git worktree list` to check for stale worktrees from previous sessions
- Clean up stale worktrees with `git worktree prune`
- Ensure your Git version supports worktrees (`git --version`, requires Git 2.5+)

### Agent takes too long

- Reduce task complexity — Arena tasks should be focused and well-defined
- Use fewer agents or models with lower latency
- Stop the Arena run manually if an agent is taking too long

### Applying winner fails

- Check for uncommitted changes in your main working directory that might conflict
- The diff is applied as a patch — merge conflicts are possible if your working directory changed during the session

## Limitations

Agent Arena is experimental. Current limitations:

- **In-process mode only**: Split-pane display via tmux or iTerm2 is not yet available. All agents run within a single terminal window with tab switching.
- **No diff preview before selection**: You can view each agent's conversation history, but there is no unified diff viewer to compare solutions side-by-side before picking a winner.
- **No worktree retention**: Worktrees are always cleaned up after selection. There is no option to preserve them for further inspection.
- **No session resumption**: Arena sessions cannot be resumed after exiting. If you close the terminal mid-session, worktrees remain on disk and must be cleaned up manually via `git worktree prune`.
- **Maximum 5 agents**: The hard limit of 5 concurrent agents cannot be changed.
- **Git repository required**: Arena requires a Git repository for worktree isolation. It cannot be used in non-Git directories.

## Comparison with other multi-agent modes

Agent Arena and the experimental Agent Team runtime serve different multi-agent workflows. Agent Swarm remains a planned mode.

|                   | **Agent Arena**                                        | **Agent Team**                                     | **Agent Swarm** (planned)                                |
| :---------------- | :----------------------------------------------------- | :------------------------------------------------- | :------------------------------------------------------- |
| **Goal**          | Competitive: Find the best solution to the _same_ task | Collaborative: Tackle _different_ aspects together | Batch parallel: Dynamically spawn workers for bulk tasks |
| **Agents**        | Pre-configured models compete independently            | Teammates collaborate with assigned roles          | Workers spawned on-the-fly, destroyed on completion      |
| **Communication** | No inter-agent communication                           | Direct peer-to-peer messaging                      | One-way: results aggregated by parent                    |
| **Isolation**     | Full: separate Git worktrees                           | In-process teammates with a shared task list       | Lightweight ephemeral context per worker                 |
| **Output**        | One selected solution applied to workspace             | Synthesized results from multiple perspectives     | Aggregated results from parallel processing              |
| **Best for**      | Benchmarking, choosing between model approaches        | Research, complex collaboration, cross-layer work  | Batch operations, data processing, map-reduce tasks      |

## Next steps

Explore related approaches for parallel and delegated work:

- **Lightweight delegation**: [Subagents](./sub-agents.md) handle focused subtasks within your session — better when you don't need model comparison
- **Collaborative execution**: [Multi-Agent Coordination](./multi-agent-coordination.md) uses Agent Team for shared tasks and teammate messaging
- **Manual parallel sessions**: Run multiple Qwen Code sessions yourself in separate terminals with [Git worktrees](https://git-scm.com/docs/git-worktree) for full manual control

---

### Users Features Code Review (`docs/users/features/code-review.md`)

# Code Review

> Review code changes for correctness, security, performance, and code quality using `/review`.

## Quick Start

```bash
# Review local uncommitted changes
/review

# Review a pull request (by number or URL)
/review 123
/review https://github.com/org/repo/pull/123

# Review and post inline comments on the PR
/review 123 --comment

# Review local changes and apply the findings to your working tree
/review --fix

# Continue a review of the same PR that was interrupted, instead of starting over
/review 123 --resume

# Review a specific file
/review src/utils/auth.ts

# Quick unverified pass (no subagents)
/review --effort low
/review 123 --effort medium
```

If there are no uncommitted changes, `/review` will let you know and stop — no agents are launched.

## Effort Levels

`--effort low|medium|high` trades depth for speed:

| Level    | What runs                                                                                                                                                   | Findings cap        | Verdict                             | Posts to PR      |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------- | ---------------- |
| `low`    | 3-6 directed inline angles over the diff (scaled by diff size) plus a gap sweep and an under-floor re-pass — no subagents, no build/test, no project rules  | 10 (unverified)     | None                                | Never            |
| `medium` | The high pipeline minus its most expensive passes: the parallel finder fan-out over a reduced dimension set, plus build/test and a single verification pass | Uncapped (verified) | Approve capped at Comment           | Never            |
| `high`   | Full pipeline: up to 16 parallel agents → sharded verification → iterative reverse audit                                                                    | Uncapped (verified) | Approve / Request changes / Comment | With `--comment` |

`/review` resolves effort in this order: an explicit `--effort`, the last level explicitly typed for this project, the operator `review.effort` setting, then the built-in target default (**high** for PR reviews, **medium** for local and file reviews). When a remembered level applies, `/review` announces it before work begins; type a new `--effort` to replace it. An effective `--comment` forces high (posted comments must survive verification) — on a non-PR target `--comment` is ignored with a warning and does **not** change the effort. Medium keeps the security and test-coverage agents and build/test, and drops the adversarial personas, the language-pitfall and wrapper/proxy specialists (Agents 1d/1e), the diff-specialist finders and the reverse audit — so a subtle Critical only the second look would surface can slip; use `--effort high` for security-sensitive or pre-release reviews. Only `low` is unverified. Worktree isolation applies to same-repo PR reviews; cross-repo PRs run in lightweight mode (diff-only, no worktree or build/test). The low pass is labeled unverified, emits no verdict, and never writes the incremental review cache, so a later `--effort high` run is never skipped as "already reviewed"; medium is verified but its Approve is capped at Comment, because nothing looked twice for what the first pass missed. The diff-obtaining mechanics are identical at every level — PR reviews always use the isolated worktree and the same base resolution, so the review is never against the wrong base. One scope difference remains: the incremental cache is high-only, so a high re-review may cover just the new commits (`lastCommitSha..HEAD`) while low/medium always review the full PR diff.

## How It Works

The `/review` command runs a multi-stage pipeline:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Steps 3A/3B/4/5 are the high-effort pipeline; at `--effort low|medium` a single inline pass (Step 3C) replaces them.

### Review Agents

| Agent                             | Focus                                                                                                                                                                                                                                                                                           |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Agent 0: Issue Fidelity           | Linked issue evidence, root-cause ownership, and whether the PR solves the reported problem                                                                                                                                                                                                     |
| Agent 1a: Line-by-line scan       | Walks every hunk plus its enclosing function: wrong conditions, off-by-one, missing `await`, edge cases, race conditions                                                                                                                                                                        |
| Agent 1b: Removed-behavior audit  | Walks every deleted/replaced line: names the invariant it enforced and hunts for where the new code re-establishes it — including removed **exports**, whose replacement often lives in another file and quietly changed a default. In 3B it runs whole-diff (chunk agents keep the local half) |
| Agent 1c: Cross-file tracer       | Walks every changed symbol's callers (consumer direction) and every added field's read sites (producer direction), plus same-PR callee changes                                                                                                                                                  |
| Agent 1d: Language-pitfall scan   | Carries the classic-footgun checklist for the diff's language (`==` coercion, falsy-value traps, loop-variable capture, mutable defaults, nil-map writes, SQL concatenation, DST arithmetic) and pattern-matches every hunk against it                                                          |
| Agent 1e: Wrapper/proxy routing   | For every type the diff adds or modifies that wraps another (cache, proxy, decorator, adapter): every method routes through the wrapped instance, and the wrapper forwards every method callers use. Rostered only when the diff signals a wrapping type                                        |
| Agent 2: Security                 | Injection, XSS, SSRF, auth bypass, sensitive data exposure                                                                                                                                                                                                                                      |
| Agent 3a: Reuse & duplication     | Does the codebase already have this? Greps for the behavior, names the existing helper to call instead, and flags dead code the diff leaves behind                                                                                                                                              |
| Agent 3b: Altitude & abstraction  | Is the fix at the right depth — or a bandaid on shared infrastructure, a downstream compensation for an upstream bug, or an abstraction serving one call site?                                                                                                                                  |
| Agent 3c: Consistency & clarity   | Sibling consistency (a guard one member of a parallel family has but its twin lacks), convention drift against a cited local example, misleading names/comments, needless complexity                                                                                                            |
| Agent 4: Performance & Efficiency | N+1 queries, memory leaks, unnecessary re-renders, bundle size                                                                                                                                                                                                                                  |
| Agent 5: Test Coverage            | Untested code paths in the diff, missing branch coverage, weak assertions                                                                                                                                                                                                                       |
| Agent 6: Undirected Audit         | 3 parallel personas (attacker / 3am-oncall / maintainer) — catches cross-dimensional issues — plus the counter-frame audit (6d, PR targets at high effort): reads the PR description only to EXCLUDE its nominated topics and replay its motivating incident the day after the merge            |
| Agent 7: Build & Test             | Runs build and test commands, reports failures                                                                                                                                                                                                                                                  |
| Agent 8: Diff-specialized finders | 0-2 extra finders written per-review when the diff concentrates in a domain with known failure modes (reconnect logic, module loaders, schedulers, codecs)                                                                                                                                      |
| `prose-exec`: Prose execution     | When the diff touches an instruction file (a `SKILL.md`, `AGENTS.md`/`CLAUDE.md`/`QWEN.md`, an agent or command definition, a `prompts/` file) and the review has a tree: EXECUTES the changed instructions in a disposable copy and files the gap between the run and what the prose promises  |

The three Correctness agents are **procedural**: each is defined by how it walks the diff (line-by-line / deleted lines / cross-file edges), not by a bug taxonomy — so their coverage is complementary instead of overlapping. Two further dedicated angles (1d/1e) split the language-pitfall checklist and wrapper/proxy routing out of the line-by-line walk: a checklist pattern-match and a structural routing expectation are different attention modes, and folded into the walk they were diluted by its rhythm. The same reasoning splits **code quality into three** (3a/3b/3c): one agent holding a six-item checklist finishes one item — measured on a heavily-rewritten file, one agent holding an eight-item checklist found 1 of 5 defects and the same model split three ways found all 5 — so the quality checklist is cut where the questions genuinely differ. All agents run in parallel (Agent 1 launches 3 procedural variants and 2 dedicated angles, Agent 3 launches 3 checklist slices, and Agent 6 launches 3 persona variants and the counter-frame audit 6d concurrently, totaling up to 17 parallel tasks for same-repo PR reviews — Agent 1e runs only when the diff signals a wrapping type — plus 0-2 Agent 8 finders when the diff's domain calls for them, so 16-19 in practice, and one more (`prose-exec`) when the diff touches an instruction file; Agent 0 and 6d are skipped for local-diff and file-path reviews, which run 14-17; cross-repo lightweight mode also skips Agents 1c and 7, running 14-17 with the PR identity and 12-15 without it).

Every finding must state a **failure scenario** — the concrete input, state, or timing that triggers it and the wrong outcome that results (for quality findings, the concrete cost instead). A finding that cannot name its scenario is dropped at the source, and verification re-traces the claimed scenario through the real code rather than judging the finding's prose.

Once a PR carries more than 500 lines of **source** change — or more than 3 200 diff lines in total, past which the sixteen whole-diff readers are each too diluted to read carefully (an attention bound, not a promise of fewer calls — heavy files and specialized finders can make 3B cost more) — this dimension fan-out is replaced by a **territory × dimension** fan-out: the diff is split into ~400-line chunks — boundaries fall on hunk boundaries, and a hunk too large to fit is split only at a top-level declaration, never inside a function — and each chunk gets its own agent that applies every review dimension to that chunk alone.

The gate deliberately counts source lines rather than diff lines. Test code, prose and lockfiles dominate diff size — across this repo's last 40 merged PRs the median diff is 41% tests — so a gate on raw size would carve a 173-line production change into territories just because it shipped 489 lines of new tests, leaving that production code with one reviewer instead of fifteen lenses (the diff-reading dimension agents — seventeen minus Issue Fidelity and Build & Test). Chunking still covers every line either way, tests included; what the gate decides is how many reviewers there are and what each is asked to do. Fifteen diff-reading lenses all walking one large diff read the same early hunks fifteen times over; one agent per chunk means every line of the diff has exactly one accountable reviewer. Each chunk agent returns a `Covered:` receipt, and a chunk with no receipt is re-reviewed before the run proceeds — so "no blockers" can never be reported over code that nobody read.

A **source** file that is largely rewritten (an existing file of 300+ lines that is now 40%+ new, or has 800+ changed lines) also gets **three whole-file invariant agents**. Test and generated files never qualify — the checklist asks about fields, timers, and error taxonomies, which a rewritten test file does not have. Its bugs are usually not inside any one hunk but _between_ the new lines — a timer armed near the top of the file and a teardown path two thousand lines below. Each agent reads the whole post-change file and walks two or three items of a fixed checklist: mutable fields cleared on every exit path, timers cancelled on every close (and cancellation not discarding captured data), map inserts matched by deletes, retry counters incremented at every entry, status return values actually checked, error codes exhaustively classified permanent vs transient, config fields honoured on every path, and early returns that skip a required side effect.

The checklist is split three ways on purpose. Handing one agent all eight checks over a 2 400-line file gets one of them done properly; three agents with two or three checks each get all of them done. Chunk agents do not substitute for this — on PR #6457 they held every one of these defects inside their assigned territory and reported none. What they lacked was not the lines but the question.

Findings are verified in **sharded batches** (at most 8 findings per verification agent, all launched together). A verifier may reject a Critical only by quoting the code that contradicts it (or when the diff's own comments document the flagged behavior as deliberate); anything less certain is downgraded to low confidence rather than deleted — a silently rejected Critical is invisible to every later stage, while a downgraded one still reaches a human. The bar applies to the shape of every rejection: it must be constructible from the code — quote the line the finding misreads, prove the claimed state impossible from a type, constant, or invariant, cite the in-diff guard that covers the trigger, or match a pure-style change with no observable effect — or otherwise match an exclusion criterion — and "too speculative" is never one of them. A finding whose failure scenario names a state the code does not exclude is plausible by default: a concurrency race, nil/undefined on a rare-but-reachable path, a falsy zero or empty collection treated as missing, an off-by-one on an unexcluded boundary, a retry storm or partial failure, a regex or allowlist that lost an anchor. A rejection that constructs none of the four grounds downgrades instead of dropping. After verification, **iterative reverse audit** hunts for gaps, fanned out one auditor per chunk per round, each with the cumulative finding list. The loop stops after **two consecutive dry rounds** (or at the plan's round cap — reported as such rather than as convergence). That cap follows the diff's topology: **10** on a small diff, where a round is a single auditor; **5** on a chunked one, where it is one auditor per chunk; and **3** on a huge diff (≥ 3000 effective lines) _when the run has an explicit deadline_ (CI's, or `--deadline` at capture), because five ~90-minute rounds do not fit a six-hour CI ceiling and a review killed mid-flight posts nothing — under the plan's default wall a huge diff keeps the chunked cap of 5. Capture prices the tier from the clock it sees and records it; a reader may only lower a recorded value inside its band (a plan captured under CI's epoch keeps 3 in a continuation resumed without that epoch; one captured with no clock is cut to 3 if the run later gains one), and a value outside the band falls back to the tier. An operator can lower whichever cap applies for every review with the `review.reverseAuditRounds` setting; it can never raise one. One dry round is not evidence of convergence, and reverse-audit findings are verified like any other.

## Severity Levels

| Severity         | Meaning                                                             | Posted as PR comment?      |
| ---------------- | ------------------------------------------------------------------- | -------------------------- |
| **Critical**     | Must fix before merging (bugs, security, data loss, build failures) | Yes (high-confidence only) |
| **Suggestion**   | Recommended improvement                                             | Yes (high-confidence only) |
| **Nice to have** | Optional optimization                                               | No (terminal only)         |

Low-confidence findings appear in a separate "Needs Human Review" section in the terminal and are never posted as PR comments.

## Worktree Isolation

When reviewing a PR, `/review` creates a temporary git worktree (`.qwen/tmp/review-pr-<number>`) instead of switching your current branch. This means:

- Your working tree, staged changes, and current branch are **never touched**
- Dependencies are installed in the worktree (`npm ci`, etc.) so build/test work
- Build and test commands run in isolation without polluting your local build cache
- If anything goes wrong, your environment is unaffected — just delete the worktree
- The worktree is automatically cleaned up after the review completes
- If a review is interrupted (Ctrl+C, crash), the next `/review` of the same PR automatically cleans up the stale worktree before starting fresh. If the interrupted session still leaves its lease behind — a hard kill that skips this, or a multi-prompt review interrupted during a later prompt — `/review` refuses and names the lease file to delete. Clean stops release it: a finished review and the early stops (empty diff, no new changes since the last review) all run `cleanup`, which releases the lease
- The worktree is leased to its session: a second `/review` of a PR that is already under review refuses to start (naming the holder) rather than tear down the running review's worktree
- Review reports and cache are saved to the main project directory (not the worktree)
- Steps that **modify** code to measure something — the test-efficacy probe's mutants, and a verifier's probe of a specific finding — each run in their own throwaway worktree beside it (`…-probe`, `…-scratch-<agent>`), so one agent's experiment is not visible to the others reading the shared tree. As a backstop, every agent in each wave is also told which paths (if any) differ from the commit under review at the moment it was launched, and that a failure confined to those paths is not a finding. All of these trees are swept along with the worktree at the end of the review.

## Cross-repo PR Review

You can review PRs from other repositories by passing the full URL:

```bash
/review https://github.com/other-org/other-repo/pull/456
```

This runs in **lightweight mode** — no worktree, no build/test. The review is based on the diff text only (fetched via GitHub API). PR comments can still be posted if you have write access.

| Capability                                                                    | Same-repo | Cross-repo                     |
| ----------------------------------------------------------------------------- | --------- | ------------------------------ |
| LLM review (Agents 0, 1a, 1b, 1d, 1e, 2-6 + verify + iterative reverse audit) | ✅        | ✅                             |
| Agent 1c: Cross-file tracer                                                   | ✅        | ❌ (no local codebase to grep) |
| Agent 7: Build & test                                                         | ✅        | ❌ (no local codebase)         |
| Agent 8: Diff-specialized finders (0-2, when the domain calls for it)         | ✅        | ✅ (needs only the diff)       |
| PR inline comments                                                            | ✅        | ✅ (if you have write access)  |
| Incremental review cache                                                      | ✅        | ❌                             |

## PR Inline Comments

Use `--comment` to post findings directly on the PR:

```bash
/review 123 --comment
```

Or, after running `/review 123`, type `post comments` to publish findings without re-running the review.

**What gets posted:**

- High-confidence Critical and Suggestion findings as inline comments on specific lines, each prefixed with `**[Critical]**` or `**[Suggestion]**` so blockers are distinguishable from recommendations
- Where the fix is a single localized edit, a ` ```
/* Detailed source-code truncated for AI context efficiency. */
```bash
/review --fix                 # local uncommitted changes
/review src/auth.ts --fix     # a single file
```
/* Detailed source-code truncated for AI context efficiency. */
```bash
/review 123 --resume
```
/* Detailed source-code truncated for AI context efficiency. */
```bash
export QWEN_REVIEW_ASSETS_REPO=your-org/your-repo   # a repo you can push to
/review 123 --comment
```
/* Detailed source-code truncated for AI context efficiency. */
```json
{
  "version": 1,
  "label": "Example repository",
  "rules": [
    {
      "paths": ["packages/*/src/**"],
      "domains": ["runtime"],
      "relatedPaths": ["packages/runtime/src/**"],
      "recommendedTests": ["npm run test:runtime"],
      "requiredConfigurations": ["debug"],
      "requiredAgents": ["test-matrix"],
      "unverifiedDimensions": ["Alternate runtime was not exercised"],
      "verificationNotes": ["Use the repository native test runner"]
    }
  ]
}
```
/* Detailed source-code truncated for AI context efficiency. */
```markdown
# Review Rules

- All API endpoints must validate authentication
- Database queries must use parameterized statements
- React components must not use inline styles
- Error messages must not expose internal paths
```

## Incremental Review

When reviewing a PR that was previously reviewed, `/review` only examines changes since the last review:

```bash
# First review — full review, cache created
/review 123

# PR updated with new commits — only new changes reviewed
/review 123
```

### Cross-model review

If you switch models (via `/model`) and re-review the same PR, `/review` detects the model change and runs a full review instead of skipping:

```bash
# Review with model A
/review 123

# Switch model
/model

# Review again — full review with model B (not skipped)
/review 123
# → "Previous review used qwen3-coder. Running full review with gpt-4o for a second opinion."
```
/* Detailed source-code truncated for AI context efficiency. */
```
.qwen/reviews/2026-04-06-143022-pr-123.md
.qwen/reviews/2026-04-06-150510-local.md
```
/* Detailed source-code truncated for AI context efficiency. */
```bash
qwen review run [target] [--json] [--fail-on request-changes] [--comment] [--resume] [--quiet]
```

`target` is a PR number, a PR URL, or a file path; omit it to review the local working tree. The command runs this build's own CLI non-interactively (with stdin closed, so slash-command detection survives), streams the child's progress to **stderr**, and prints the verdict to **stdout** — or, with `--json`, the full result object. The verdict is read from the artifact `compose-review` writes (the same JSON the skill treats as the verdict authority), never parsed from the model's prose.

The exit code is the contract a gate should read:

| Exit | Meaning                                                                                           |
| ---- | ------------------------------------------------------------------------------------------------- |
| `0`  | The review completed (whatever it decided)                                                        |
| `1`  | It never reached a verdict — the child failed, timed out, or left no composed artifact            |
| `3`  | It completed with `REQUEST_CHANGES` **and** `--fail-on request-changes` was set (opt-in blocking) |

`3` (not `2`) lets a gate distinguish "the review is blocking" from "the tool broke" — yargs already uses `1` for usage errors — without parsing any output. `--timeout-minutes` (default 120, floored at 1) terminates a hung review and exits `1`, and cancelling the command (Ctrl+C / SIGTERM) terminates the review's process group rather than orphaning it.

`--resume` continues an interrupted review of the same PR instead of starting over — when a long local run dies part-way (a dropped connection, a timeout, a killed terminal), the retry would otherwise re-fetch, re-chunk and re-launch agents whose work is already on disk. It is safe to pass unconditionally on a retry: `fetch-pr` rules on the on-disk state itself (worktree still at the fetched SHA and clean, diff bytes unchanged, PR head unmoved, resume cap unspent) and silently falls back to a fresh review whenever anything no longer matches, so the flag never fails a run that could start over. When the current invocation has only the built-in target default, a continuation stays pinned to the interrupted run's recorded effort. An explicit `--effort`, a project-remembered level, the operator `review.effort` setting, or an effective `--comment` supplies a required level; a mismatch refuses the resume and runs fresh at that level. PR targets only (a local review's diff is captured from a live working tree, which has no stable interrupted state to continue). Resume is a **local convenience**: the repository's own CI review workflow does **not** resume — each retry re-runs fresh, because a CI attempt runs no-sandbox and its worktree is deleted on exit, leaving no interrupted state to continue.

A time-budgeted run can also export a **soft** deadline so the review stops its open-ended reverse-audit loop while there is still time to verify, compose and post: `QWEN_REVIEW_DEADLINE_EPOCH` is the Unix-seconds moment the run will be killed, and `QWEN_REVIEW_DEADLINE_RESERVE_SECONDS` (default 4800; `0` keeps only the round estimate) is the tail that must remain for the last round's verification, `compose-review` and submission. When the remaining budget no longer fits another round plus that tail, the round builder refuses to build it, and the composed verdict discloses the truncated audit (an otherwise-Approve verdict is capped at Comment). Without that variable the review uses the wall its plan recorded at capture: `--deadline <minutes>` on `qwen review fetch-pr` / `capture-local` / `plan-diff`, or by default **8 hours** on a small (Step 3A) diff, **12** on a large (3B) one and **16** on a huge one — sized by the diff's own lines, so a critical-posture fix-audit round keeps the wall its delta's size earns even though it fans out per territory and takes the chunked round cap. That default is a liveness bound — sized above what a healthy run at the round cap spends — so it ends a loop that has stopped converging and leaves a run that keeps working untouched; it is charged in elapsed time, idle included, so a session left overnight spends it, and because the next round is priced from the previous round's admission, a multi-hour pause between rounds can stop a run with time still on the wall (a `--resume` from a new session restarts both the wall and the pricing; **Review Deadline** below gives the ceilings). It stops the loop at the next round admission or verify build; it does not interrupt a build already running. A plan-recorded wall keeps a third of itself (capped at 4800 seconds, floored at the compose floor) as the tail reserve unless the reserve variable is set; a `--deadline` that cannot hold a convergence (two rounds plus that reserve — at or under ninety minutes, or more when the shell's reserve or compose-floor overrides raise the bar; that second leg is skipped while the environment exports a well-formed epoch, since the flag is inert then, and the wall it records is priced again in whatever shell a later env-less `--resume` runs in) is refused up front, and a wall just above it can still stop before round 1 once the fan-out and verification have spent it. When the wall can no longer admit a rebuild the coverage check would otherwise prescribe (a reverse-audit round, or a verifier shard under the compose floor), `compose-review` withholds that `FIX:` line and prints a `NOTE:` line saying so, with the gate's arithmetic; the gap is still disclosed in the posted body. The plan wall bounds the round builder and the verifier; two readers stay on the environment alone — the generated review workflow's launch allowance, and `qwen review run`'s own `--timeout-minutes` kill, which is not exported as a deadline. The plan stores the wall as a duration from the attempt's start, so a `--resume` from a new session renews it (a same-session resume continues the attempt and its wall; the flag itself is ignored on a resumed run — only its grammar and the default rule are checked — and the run says what wall the plan holds and how much of it is left, or that it has run out, and which builds the gates will now refuse; a wall can only run out on a same-session resume, or when the ledger failed to record the attempt, so the cheapest way past it is to resume from a new session, which renews the wall and keeps the worktree; a fresh review without `--resume` records a new wall too, and `--deadline none` on that fresh capture records none). `--deadline none` records no wall and leaves the loop bounded by the round cap alone — unless the environment exports an epoch, which clocks the run regardless. The `/review` slash command does not take the flag: it warns and drops it together with a value that could be one (whole minutes, or `none` — so a PR number typed right after it is dropped too), while any other value after it is disposed of like an invalid `--effort` value (a PR URL becomes the target; a typo is reported, and survives only as a sole file-shaped token), and a skill-driven run always uses the default. The huge tier's round reduction (3 rounds instead of 5) keys on an explicit deadline only — the environment's or the flag's — never on the default. A malformed environment deadline falls through to the plan's wall; a malformed plan field leaves the review ungated — the outer timeout still bounds the run.

Nested inside that reserve is a smaller **compose floor**, `QWEN_REVIEW_DEADLINE_COMPOSE_FLOOR_SECONDS` (default 1200; `0` disables this gate entirely, at every point including past the deadline). The reserve is one number covering "verify the last round **plus** compose **plus** submit", which fits a normal per-finding re-trace but not a security review whose verification re-runs real filesystem/git workloads without bound. So the verifier — not the round builder — is gated on this floor: once the floor or less remains, `agent-prompt --role verify` refuses to build (a `VERIFY BUDGET:` line, exit **4**), the findings in hand keep their unverified tag (which caps the verdict), and `compose-review` and submission run. Under the plan's own rule the floor never exceeds the reserve (a plan-recorded wall's reserve can sit exactly on it), and the round gate prices the round on top of the reserve, so a healthy run hits the reverse-audit gate first and never reaches it (an explicit `QWEN_REVIEW_DEADLINE_RESERVE_SECONDS` below the floor — `0` is the documented escape hatch — is the operator's choice, and then the verifier's gate can fire first); it is the cover for the one span the reserve cannot bound.

## Cross-file Impact Analysis

A dedicated cross-file tracer (Agent 1c) owns this walk end-to-end. When code changes modify exported functions, classes, or interfaces, it searches for all callers and checks compatibility:

- Parameter count/type changes
- Return type changes
- Removed or renamed public methods
- Breaking API changes

It also walks the **producer direction**: every field, option, or optional parameter the diff adds is traced to its read sites — including files the diff never touches. A live code path reading a field that nothing populates means the feature it gates silently does nothing, and that is flagged as Critical at the read site.

For large diffs (>10 modified symbols), the caller-direction analysis prioritizes functions with signature changes; the producer direction is never budget-limited, because an unchanged signature is exactly its point.

## Review Budget

The parts of the pipeline that are elastic in diff size are scaled from it, and the scaling is written into the diff plan so every stage reads one number rather than each deciding for itself:

| Budget field     | What it scopes                                   | How it scales                                                      |
| ---------------- | ------------------------------------------------ | ------------------------------------------------------------------ |
| `inlineAngles`   | How many `low` angles run (Step 3C)              | 3, plus one per 60 source lines, capped at the 6 angles that exist |
| `candidateFloor` | When `low` owes one deterministic recall re-pass | `min(changed files, 4)`                                            |
| `sweep`          | Whether `low`'s gap sweep runs                   | Off below 25 source lines                                          |
| `specialistCap`  | The Agent 8 ceiling                              | 0 below 80 source lines, otherwise 2                               |
| `verifyShard`    | Findings per verification agent                  | Flat at 8 — a property of the verifier, not of the diff            |

Two things it deliberately does not do. It **never scales a dimension away**: which agents a review owes is decided by the roster, which reads the effort level, so a small diff still gets its security pass and its test-coverage pass. And it reads **source** lines, not diff lines — a 40-line production change shipping 900 lines of new tests is a small change, and the same reasoning already governs the territory-fan-out gate.

Why the floors are where they are: on a nine-line typo fix, six inline walks are five walks over nothing, and the sweep — a fresh reader hunting what the first pass did not get to — has nothing to hunt when the first pass got to all of it. When a low-effort pass stays below `candidateFloor`, it takes one deterministic second look at every coverable hunk in the largest changed source file (falling back to the largest coverable file of any kind) and every coverable removed block. Files touching an uncoverable chunk are excluded from the target selection, and the pass always emits a receipt naming its target, new-candidate count, and any uncoverable chunks; when no file is coverable, it still checks the coverable removed blocks and discloses the missing target. The floor is a stopping signal, not a quota: a clean diff can still report no findings after that re-pass. Agent 8's floor is the substantive one: "one domain dominates the diff" is a judgement, and a judgement made about forty lines finds a dominant domain every time, because forty lines are usually all one thing.

## Review Deadline

Every review records a wall clock in its plan unless it was captured with `--deadline none` — `--deadline <minutes>` on `qwen review fetch-pr`, `capture-local` or `plan-diff`, otherwise **8 hours** on a small diff, **12** on a large one and **16** on a huge one — and a `QWEN_REVIEW_DEADLINE_EPOCH` exported by CI replaces it. At `high` effort the wall ends the reverse-audit loop at the first round it can no longer admit, and at `high` and `medium` effort it stops the verifier once no more than the compose floor remains; it never interrupts an agent that is already running. A review stopped this way composes its verdict with the stop disclosed and the verdict capped, verifies the last round's findings only while more than the compose floor remains, and reports what it proved like any other review. Running `/review` again does not continue the stopped audit: at `high` effort a PR review follows the Incremental Review rules above and may report nothing new on an unchanged head, and a local review with its cache may stop the same way on an unchanged working tree; at `medium` effort the review simply runs again.

The wall is elapsed time, pauses included, and a pause is charged twice: it spends the wall, and while it is the longest gap between two round admissions it is also the price of the next round. The pause that matters in practice falls after the first audit pair — rounds 1 and 2, which launch together — and before round 3. Under the default reserve, a review whose pair launched the moment its capture finished can pause for at most:

| Diff            | Default wall | Reserve | Longest pause after the first audit pair |
| --------------- | ------------ | ------- | ---------------------------------------- |
| Small (Step 3A) | 8 h          | 80 min  | 3 h 20 min                               |
| Large (Step 3B) | 12 h         | 80 min  | 5 h 20 min                               |
| Huge            | 16 h         | 80 min  | 7 h 20 min                               |

Those are ceilings, and the same rule covers any wall of 4 hours or more under the default reserve: at most (wall − 80 min) / 2 after a pair launched at capture. The pause is counted from the pair's launch, so it includes the pair's own audits and the verifiers that ride with it, and every minute the review spent before the pair launched takes half a minute off. Each later gap is held to at most half of what was left above the reserve at the previous admission, and can be held to less, for instance once an earlier gap was longer than that half. The precedence rules, the environment variables that set the wall and the reserve, and the up-front refusal of a too-short `--deadline` are described under Headless runs above. The `/review` slash command takes no `--deadline`, so a review started from a session gets a different wall only through those environment variables, and they apply to every review the session runs: `QWEN_REVIEW_DEADLINE_EPOCH` is one absolute moment shared by all of them, and like an explicit `--deadline` it cuts a huge diff's round cap from 5 to 3. A `--resume` from a new session restarts the wall and the pricing for an interrupted PR review.

## Token Efficiency

The high-effort pipeline bounds each stage (shard size, audit rounds), but total calls scale with findings — `ceil(F/8)` verification shards — and, under 3B, with chunk count (reverse audit runs per chunk per round). Typical 3A profile:

| Stage                            | LLM calls                       | Notes                                                                                                                                                                                                                                                                                                                                                                          |
| -------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Review agents (Step 3)           | 17 (+0-2)                       | Run in parallel; Agent 1e only when the diff signals a wrapping type (16 without it); cross-repo skips Agents 1c and 7 (15 while the plan carries the PR identity, 13 without it — Agent 0 and 6d drop together); local/file skips Agent 0 and the counter-frame audit 6d (15); one more (`prose-exec`) on a diff that touches an instruction file, when the review has a tree |
| Sharded verification (Step 4)    | ceil(F/8)                       | F = findings; at most 8 per verification agent, launched together                                                                                                                                                                                                                                                                                                              |
| Iterative reverse audit (Step 5) | 2-10 (3A); rounds × chunks (3B) | Two consecutive dry rounds to stop; the cap follows the topology — 10 on a small diff, 5 on a chunked one, 3 on a huge one under an explicit deadline. 3B fans out one auditor per chunk per round                                                                                                                                                                             |
| **Total**                        | **~20-31 (~16-29)**             | 3A same-repo: ~20-31 (typical ~20-22); cross-repo or local/file: ~16-29 (the floor is the identity-less cross-repo roster of 13); one fewer when Agent 1e is not rostered, one more when `prose-exec` is owed; 3B scales with chunks (see DESIGN.md)                                                                                                                           |

Most PRs converge to the lower end of the range; the caps prevent runaway cost on pathological cases. At `--effort low` the review runs entirely inline — **0 subagent calls** — walking the diff once per angle instead of once in total.

## What's NOT Flagged

The review intentionally excludes:

- Pre-existing issues in unchanged code (focus on the diff only)
- Style or formatting a formatter would auto-normalize, or naming matching your codebase conventions — but NOT substantive issues a linter or type checker would flag (unused variables, unreachable code, type errors), which are in scope
- Subjective "consider doing X" suggestions without a real problem
- Minor refactoring that doesn't fix a bug or risk
- Missing documentation unless the logic is genuinely confusing
- Issues already discussed in existing PR comments (avoids duplicating human feedback)

## Design Philosophy

> **Silence is better than noise.** Every comment should be worth the reader's time.

- If unsure whether something is a problem → don't report it
- Every finding names a concrete failure scenario (trigger → wrong outcome) or a concrete cost — a finding that can't is dropped before it reaches you
- Same pattern across N files → aggregated into one finding
- PR comments are high-confidence only (and only from high-effort, verified reviews)
- Cosmetic style/formatting matching codebase conventions is excluded

---

### Users Features Commands (`docs/users/features/commands.md`)

# Commands

This document details all commands supported by Qwen Code, helping you efficiently manage sessions, customize the interface, and control its behavior.

Qwen Code commands are triggered through specific prefixes and fall into three categories:

| Prefix Type                | Function Description                                | Typical Use Case                                                 |
| -------------------------- | --------------------------------------------------- | ---------------------------------------------------------------- |
| Slash Commands (`/`)       | Meta-level control of Qwen Code itself              | Managing sessions, modifying settings, getting help              |
| At Commands (`@`)          | Quickly inject local file content into conversation | Allowing AI to analyze specified files or code under directories |
| Exclamation Commands (`!`) | Direct interaction with system Shell                | Executing system commands like `git status`, `ls`, etc.          |

## 1. Slash Commands (`/`)

Slash commands are used to manage Qwen Code sessions, interface, and basic behavior.

### 1.1 Session and Project Management

These commands help you save, restore, and summarize work progress.

| Command          | Description                                                              | Usage Examples                                                |
| ---------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------- |
| `/init`          | Analyze current directory and create initial context file                | `/init`                                                       |
| `/summary`       | Generate project summary based on conversation history                   | `/summary` or `/summary docs/my-summary.md`                   |
| `/compress`      | Replace chat history with summary to save Tokens                         | `/compress` or `/summarize`                                   |
| `/compress-fast` | Fast compression without AI — strips old tool outputs and thinking parts | `/compress-fast`                                              |
| `/resume`        | Resume a previous conversation session                                   | `/resume` or `/continue`                                      |
| `/recap`         | Generate a one-line session recap now                                    | `/recap`                                                      |
| `/restore`       | Revert project files to the checkpoint before a tool call ran            | `/restore` (list) or `/restore <ID>`                          |
| `/delete`        | Delete a previous session                                                | `/delete`                                                     |
| `/branch`        | Fork the current conversation into a new session                         | `/branch`                                                     |
| `/fork`          | Spawn a background agent that inherits the full conversation             | `/fork <directive>`                                           |
| `/rewind`        | Rewind conversation to a previous turn                                   | `/rewind` or `/rollback`                                      |
| `/export`        | Export session history to file                                           | `/export html`, `/export md`, `/export json`, `/export jsonl` |
| `/rename`        | Rename or tag the current session                                        | `/rename My Feature` or `/tag`                                |
| `/commit`        | Create a git commit with an AI-drafted message                           | `/commit` or `/commit only stage the parser changes`          |

> [!note]
>
> Opening an HTML export loads the renderer and stylesheet for that exact Qwen Code version from `unpkg.com`. If the version has not been published or either asset cannot be reached, the file shows a load error. Markdown, JSON, and JSONL exports remain self-contained.

> [!note]
>
> `/summarize` is an alias for `/compress` (it compresses chat history — a destructive operation). To generate a non-destructive project summary instead, use `/summary`.

> [!note]
>
> `/summary` accepts an optional `[path]` argument to save the summary to a custom location within the project root. Without an argument, it saves to `.qwen/PROJECT_SUMMARY.md`. Custom-path summaries are not detected by the welcome-back flow (`ui.enableWelcomeBack`), which only reads the default `.qwen/PROJECT_SUMMARY.md` location.

### 1.2 Interface and Workspace Control

Commands for adjusting interface appearance and work environment.

| Command              | Description                                                                                                                                                                       | Usage Examples                                                                    |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `/clear`             | Clear conversation history and free up context                                                                                                                                    | `/clear`, `/reset`, `/new`                                                        |
| `/context`           | Show context window usage breakdown                                                                                                                                               | `/context`                                                                        |
| → `detail`           | Show per-item context usage breakdown                                                                                                                                             | `/context detail`                                                                 |
| `/history`           | Control history display preferences and visibility                                                                                                                                | `/history collapse-on-resume`, `/history expand-on-resume`, `/history expand-now` |
| `/diff`              | Open an interactive diff viewer showing uncommitted changes and per-turn diffs. Use ←/→ to switch between current git diff and individual conversation turns, ↑/↓ to browse files | `/diff`                                                                           |
| `/log`               | Open a commit history viewer for the workspace (Web Shell only)                                                                                                                   | `/log`                                                                            |
| `/theme`             | Change Qwen Code visual theme                                                                                                                                                     | `/theme`                                                                          |
| `/vim`               | Turn input area Vim editing mode on/off                                                                                                                                           | `/vim`                                                                            |
| `/voice`             | Toggle voice dictation input                                                                                                                                                      | `/voice`, `/voice hold`, `/voice tap`, `/voice off`, `/voice status`              |
| `/directory`         | Manage multi-directory support workspace                                                                                                                                          | `/dir add ./src,./tests`, `/dir show`                                             |
| `/cd`                | Move this session to a new working directory                                                                                                                                      | `/cd ../other-project`                                                            |
| `/editor`            | Open dialog to select supported editor                                                                                                                                            | `/editor`                                                                         |
| `/statusline`        | Open interactive [status line](./status-line.md) preset dialog                                                                                                                    | `/statusline`                                                                     |
| `/statusline <text>` | Generate a command-mode [status line](./status-line.md) via agent                                                                                                                 | `/statusline show model and git branch`                                           |
| `/terminal-setup`    | Configure terminal keybindings for multiline input                                                                                                                                | `/terminal-setup`                                                                 |

### 1.3 Language Settings

Commands specifically for controlling interface and output language.

| Command               | Description                      | Usage Examples             |
| --------------------- | -------------------------------- | -------------------------- |
| `/language`           | View or change language settings | `/language`                |
| → `ui [language]`     | Set UI interface language        | `/language ui zh-CN`       |
| → `output [language]` | Set LLM output language          | `/language output Chinese` |

- Available built-in UI languages: `zh-CN` (Simplified Chinese), `en-US` (English), `ru-RU` (Russian), `de-DE` (German), `ja-JP` (Japanese), `pt-BR` (Portuguese - Brazil), `fr-FR` (French), `ca-ES` (Catalan)
- Output language examples: `Chinese`, `English`, `Japanese`, etc.

### 1.4 Tool and Model Management

Commands for managing AI tools and models.

| Command               | Description                                                                                      | Usage Examples                                                                                            |
| --------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `/mcp`                | List configured MCP servers and tools                                                            | `/mcp`, `/mcp desc`, `/mcp nodesc`, `/mcp schema`                                                         |
| `/import-config`      | Import MCP servers from Claude configs                                                           | `/import-config all`, `/import-config claude-code`, `/import-config claude-desktop --scope user\|project` |
| `/tools`              | Display currently available tool list                                                            | `/tools`, `/tools desc`                                                                                   |
| `/skills`             | Open the Skills panel to browse, search, toggle, and launch skills                               | `/skills`, `/<skill-name>`                                                                                |
| `/learn`              | Create a reusable project skill from a file, directory, URL, video, or text                      | `/learn https://docs.example.com/api`, `/learn ./tutorial.mp4 focus on deployment`                        |
| `/curator`            | Inspect, pin, archive, or restore inactive project auto-skills                                   | `/curator`, `/curator run --dry-run`, `/curator pin <directory>`, `/curator restore <directory>`          |
| `/plan`               | Switch to plan mode or exit plan mode                                                            | `/plan`, `/plan <task>`, `/plan exit`                                                                     |
| `/approval-mode`      | Change the tool-approval mode (current session only)                                             | `/approval-mode`, `/approval-mode auto-edit`                                                              |
| → `plan`              | Analysis only, no execution (secure review)                                                      | `/approval-mode plan`                                                                                     |
| → `default`           | Require approval for edits (daily use)                                                           | `/approval-mode default`                                                                                  |
| → `auto-edit`         | Auto-approve edits (trusted environment)                                                         | `/approval-mode auto-edit`                                                                                |
| → `auto`              | Classifier-evaluated approval (autonomous)                                                       | `/approval-mode auto`                                                                                     |
| → `yolo`              | Auto-approve everything (quick prototyping)                                                      | `/approval-mode yolo`                                                                                     |
| `/peers`              | Review held peer messages; manage trusted controllers                                            | `/peers`, `/peers accept <id>`, `/peers deny all`, `/peers controllers`, `/peers revoke <id>`             |
| `/model`              | Switch model used in current session                                                             | `/model`, `/model <model-id>` (switch immediately)                                                        |
| `/model --fast`       | Set a lighter model for prompt suggestions                                                       | `/model --fast qwen3-coder-flash`                                                                         |
| `/model --voice`      | Set the model used for voice transcription                                                       | `/model --voice <model-id>`                                                                               |
| `/model --vision`     | Set the vision-bridge model used to transcribe images for a text-only main model                 | `/model --vision <model-id>`                                                                              |
| `/model --compaction` | Set the model used for chat compression                                                          | `/model --compaction <model-id>`, `/model --compaction clear`                                             |
| `/model --image`      | Set an image-generation-capable model for the built-in image generation tool                     | `/model --image <model-id>`                                                                               |
| `/effort`             | Set reasoning effort for thinking-capable models                                                 | `/effort` (opens picker), `/effort high` (low/medium/high/xhigh/max; mapped & clamped per provider)       |
| `/output-style`       | Choose the output style that shapes how responses are written                                    | `/output-style` (opens picker), `/output-style Concise`, `/output-style default` (no style)               |
| `/extensions`         | Manage extensions                                                                                | `/extensions list`, `/extensions manage`                                                                  |
| → `list`              | List installed extensions                                                                        | `/extensions list`                                                                                        |
| → `manage`            | Manage installed extensions (interactive)                                                        | `/extensions manage`                                                                                      |
| → `explore`           | Open extensions page in browser                                                                  | `/extensions explore <Gemini\|ClaudeCode>`                                                                |
| → `install`           | Install an extension from a git repo or path                                                     | `/extensions install <repo-or-path>`                                                                      |
| `/memory`             | Open the Memory Manager dialog                                                                   | `/memory`                                                                                                 |
| `/remember`           | Save a durable memory                                                                            | `/remember Prefer terse responses`                                                                        |
| `/forget`             | Remove matching entries from auto-memory                                                         | `/forget <query>`                                                                                         |
| `/dream`              | Manually run auto-memory consolidation                                                           | `/dream`                                                                                                  |
| `/hooks`              | Manage Qwen Code hooks                                                                           | `/hooks`, `/hooks list`                                                                                   |
| `/reload-plugins`     | Reload extension changes (commands, skills, agents, workflows, hooks, MCP/LSP servers) from disk | `/reload-plugins`                                                                                         |
| `/permissions`        | Manage permission rules                                                                          | `/permissions`                                                                                            |
| `/agents`             | Manage subagents                                                                                 | `/agents manage`, `/agents create`                                                                        |
| `/arena`              | Manage Arena sessions                                                                            | `/arena start`, `/arena stop`, `/arena status`, `/arena select` (alias `choose`)                          |
| `/goal`               | Set a Goal — keep working until a verifier confirms it (see [Goals](./goals.md))                 | `/goal <objective>`, `/goal edit <objective>`, `/goal pause`, `/goal resume`, `/goal clear`               |
| `/tasks`              | List background tasks                                                                            | `/tasks`                                                                                                  |
| `/workflows`          | Inspect workflow runs; cooperatively pause/resume a background run                               | `/workflows`, `/workflows <runId>`, `/workflows p <runId>`                                                |
| `/lsp`                | Show LSP server status                                                                           | `/lsp`                                                                                                    |
| `/trust`              | Manage folder trust settings                                                                     | `/trust`                                                                                                  |

> [!warning]
>
> Only install extensions (`/extensions install`) from sources you trust. Extensions can bundle MCP servers, skills, and commands that run with the same permissions as Qwen Code itself — they can access your files, API keys, and conversation data. They can also ship workflow scripts, which run in the workflow sandbox without direct file or shell access, act through the subagents they start, and go through the Workflow tool's approval. `/extensions install` does not prompt for confirmation.

> [!warning]
>
> The `auto-edit`, `auto`, and `yolo` approval modes bypass approval prompts for tool executions. In `yolo` mode, all actions — including shell commands, file writes, and network requests — run without confirmation. Only use these modes in trusted, sandboxed, or disposable environments.

> [!note]
>
> `/workflows`, `/lsp`, and `/trust` are registered only when their feature is enabled — via the user/system-scoped `tools.workflowsEnabled` setting or `QWEN_CODE_ENABLE_WORKFLOWS=1` env var, the `--experimental-lsp` CLI flag, and the `security.folderTrust.enabled` setting respectively. Workspace values for `tools.workflowsEnabled` are ignored. When disabled these commands won't appear and will report an unknown command. Similarly, `/dream` and `/forget` are registered only when managed auto-memory is available; without it they won't appear.

> [!note]
>
> A skill from an installed extension is a slash command too, and its name carries its owner: `/rust:pdf`, not `/pdf`. The bare form is not an alias — if another skill is named `pdf`, `/pdf` runs that skill instead. `slashCommands.disabled` gates such a command under either spelling, so an entry written before the name carried the owner still bites. See [How extension Skills are named](./skills.md#how-extension-skills-are-named).

### 1.5 Built-in Skills

These commands invoke bundled skills that provide specialized workflows.

| Command       | Description                                                   | Usage Examples                                                            |
| ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `/review`     | Multi-agent code review (12 parallel agents at high effort)   | `/review`, `/review 123`, `/review 123 --comment`, `/review --effort low` |
| `/coordinate` | Coordinate read-only workers and one optional worktree writer | `/coordinate investigate and fix the authentication regression`           |
| `/loop`       | Run a prompt on a recurring schedule                          | `/loop 5m check the build`                                                |
| `/goal-draft` | Turn a fuzzy intention into a verifiable `/goal` objective    | `/goal-draft make the auth tests pass`                                    |
| `/simplify`   | Review recent changes and apply safe cleanup edits directly   | `/simplify`, `/simplify focus on duplication`                             |
| `/qc-helper`  | Answer questions about Qwen Code usage and configuration      | `/qc-helper how do I configure MCP?`                                      |

See [Code Review](./code-review.md) for full `/review` documentation.

### 1.6 Side Question (`/btw`)

The `/btw` command allows you to ask quick side questions without interrupting or affecting the main conversation flow.

| Command                | Description                           |
| ---------------------- | ------------------------------------- |
| `/btw <your question>` | Ask a quick side question             |
| `?btw <your question>` | Alternative syntax for side questions |

**How It Works:**

- The side question is sent as a separate API call with recent conversation context (up to the last 20 messages)
- The response is displayed above the Composer — you can continue typing while waiting
- The main conversation is **not blocked** — it continues independently
- The side question response does **not** become part of the main conversation history
- Answers are rendered with full Markdown support (code blocks, lists, tables, etc.)

**Keyboard Shortcuts (Interactive Mode):**

| Shortcut             | Action                                              |
| -------------------- | --------------------------------------------------- |
| `Escape`             | Cancel (while loading) or dismiss (after completed) |
| `Space` or `Enter`   | Dismiss the answer (when input is empty)            |
| `Ctrl+C` or `Ctrl+D` | Cancel an in-flight side question                   |

**Example:**

```
(While the main conversation is about refactoring code)

> /btw What's the difference between let and var in JavaScript?

  ╭──────────────────────────────────────────╮
  │ /btw What's the difference between let   │
  │     and var in JavaScript?               │
  │                                          │
  │ + Answering...                           │
  │ Press Escape, Ctrl+C, or Ctrl+D to cancel│
  ╰──────────────────────────────────────────╯
  > (Composer remains active — keep typing)

(After the answer arrives)

  ╭──────────────────────────────────────────╮
  │ /btw What's the difference between let   │
  │     and var in JavaScript?               │
  │                                          │
  │ `let` is block-scoped, while `var` is    │
  │ function-scoped. `let` was introduced    │
  │ in ES6 and doesn't hoist the same way.   │
  │                                          │
  │ Press Space, Enter, or Escape to dismiss │
  ╰──────────────────────────────────────────╯
  > (Composer still active)
```

**Supported Execution Modes:**

| Mode                 | Behavior                                     |
| -------------------- | -------------------------------------------- |
| Interactive          | Shows above Composer with Markdown rendering |
| Non-interactive      | Returns text result: `btw> question\nanswer` |
| ACP (Agent Protocol) | Returns stream_messages async generator      |

> [!tip]
>
> Use `/btw` when you need a quick answer without derailing your main task. It's especially useful for clarifying concepts, checking facts, or getting quick explanations while staying focused on your primary workflow.

### 1.7 Second Opinion (`/advisor`)

The `/advisor` command runs an independent, read-only review of the conversation so far and returns a structured second opinion — without performing the task or interrupting the main conversation.

| Command            | Description                            |
| ------------------ | -------------------------------------- |
| `/advisor`         | Review the conversation above          |
| `/advisor <focus>` | Focus the review on a specific concern |

**How It Works:**

- The review is sent as a separate, single-turn API call with recent conversation context (up to the last 40 messages)
- The reviewer model **cannot execute tools** — tools are stripped at the request level (the same mechanism as `/btw`), so the review never writes code or runs commands; every claim must be grounded in the visible transcript
- The main conversation is **not** interrupted; the review is shown only to you
- The review is rendered as a boxed markdown block with four fixed sections — **Verdict**, **Risks**, **Missing evidence**, and **Recommendation** — under an `/advisor · <model>` header that names the resolved reviewer model
- Unlike `/btw`, which is fire-and-forget and leaves the session usable, `/advisor` blocks input until the review returns; over a full context window with a strong reviewer this can take tens of seconds
- By default the main model is used; set [`advisorModel`](../configuration/settings.md#advisormodel) to route the review to a different (typically stronger) model — the recent transcript is sent to that model even when it uses another provider

**Example:**

```
> /advisor is my fix for the null check actually correct?

  Consulting advisor...

  ╭──────────────────────────────────────────────────────╮
  │ /advisor · qwen3-max                                 │
  │                                                      │
  │ Verdict                                              │
  │ The approach is sound, but the edge case at line 42  │
  │ is unverified.                                       │
  │                                                      │
  │ Risks                                                │
  │  - The fix assumes the config is always loaded; a    │
  │    startup race could leave it null.                 │
  │                                                      │
  │ Missing evidence                                     │
  │  - No test exercises the null-config path in the     │
  │    visible transcript.                               │
  │                                                      │
  │ Recommendation                                       │
  │ Add a focused unit test for the null-config branch   │
  │ before merging.                                      │
  ╰──────────────────────────────────────────────────────╯
```

The review renders in a bordered box whose header names the resolved reviewer model. An unknown `advisorModel` is not validated up front — if the provider rejects it, `/advisor` reports the failure, so check the model name; only unresolvable alias selectors (e.g. `fast` with no fast model configured) fall back to the main model. Advisor requests do not use configured model fallbacks.

**Supported Execution Modes:**

| Mode                 | Behavior                                            |
| -------------------- | --------------------------------------------------- |
| Interactive          | Renders the four-section review in the conversation |
| ACP (Agent Protocol) | Returns the review as a message result              |

> [!tip]
>
> Use `/advisor` for a second opinion before committing to a direction — it is especially useful for catching flawed assumptions, unverified claims, or risky next steps. Configure `advisorModel` to get the review from a different model than the one driving the main conversation.

> [!note]
>
> `advisorModel` is set in settings only; unlike `fastModel` and `visionModel`, it has no `/model` flag counterpart yet.

### 1.8 Session Recap (`/recap`)

The `/recap` command generates a short "where you left off" summary of the
current session, so you can resume an old conversation without scrolling
back through pages of history.

| Command  | Description                                |
| -------- | ------------------------------------------ |
| `/recap` | Generate and show a one-line session recap |

**How it works:**

- Uses the configured fast model (`fastModel` setting) when available, falling
  back to the main session model. A small, cheap model is enough for a recap.
- The recent conversation (up to 30 messages, text only — tool calls and tool
  responses are filtered out) is sent to the model with a tight system prompt.
- The recap is rendered in dim color with a `❯` prefix so it stands apart
  from real assistant replies.
- Refuses with an inline error if a model turn is in flight or another command
  is processing. If there is no usable conversation, or the underlying
  generation fails, `/recap` shows a short info message instead of a recap —
  the manual command always responds with something.

**Auto-trigger when returning from being away:**

If the terminal is blurred for **5+ minutes** and gets focused again, a recap
is generated and shown automatically (only when no model response is in
progress; otherwise it waits for the current turn to finish and then fires).
Unlike the manual command, the auto-trigger is fully silent on failure: if
generation errors or there is nothing to summarize, no message is added to
the history. Controlled by the `general.showSessionRecap` setting
(default: `false`); the manual `/recap` command always works regardless of
this setting.

**Example:**

```
> /recap

❯ Refactoring loopDetectionService.ts to address long-session OOM caused by
  unbounded streamContentHistory and contentStats. The next step is to
  implement option B (LRU sliding window with FNV-1a) pending confirmation.
```

> [!tip]
>
> Configure a fast model via `/model --fast <model>` (e.g.
> `qwen3-coder-flash`) to make `/recap` fast and cheap. Set
> `general.showSessionRecap` to `true` to enable the auto-trigger; the
> manual `/recap` command always works regardless of this setting.

### 1.9 Diff Viewer (`/diff`)

The `/diff` command opens an interactive diff viewer showing uncommitted changes and per-turn diffs. Use ←/→ to switch between the current git diff and individual conversation turns, ↑/↓ to browse files, and Enter to view inline diffs.

**How it works:**

In interactive mode, `/diff` opens a dialog with a **source picker** along the top:

- **Current** — working tree vs HEAD (`git diff HEAD`). Shows all uncommitted changes including staged, unstaged, and untracked files.
- **T1, T2, T3, …** — per-turn diffs, one tab per model turn that modified files. Most recent turns appear first. Each tab shows a preview of the original prompt for context.

The file list displays per-file stats (lines added/removed) with tags for special states (`new`, `deleted`, `untracked`, `binary`, `truncated`, `oversized`). Press Enter on a file to view its inline diff with syntax-highlighted hunks.

Per-turn diffs require file checkpointing to be enabled (on by default in interactive mode). When file checkpointing is off, only the "Current" source is available.

**Keyboard shortcuts:**

| Key       | Action                                      |
| --------- | ------------------------------------------- |
| `←` / `→` | Switch between sources (Current / T1 / T2…) |
| `↑` / `↓` | Navigate file list                          |
| `j` / `k` | Navigate file list (vim-style)              |
| Enter     | View inline diff for selected file          |
| `←` / Esc | Return to file list from inline diff view   |
| Esc       | Close the dialog                            |

**Example:**

```
┌ /diff · Turn 3 "refactor the auth middleware" ──── 3 files +45 -12 ┐
│                                                                     │
│ ◀ Current · T3 · T2 · T1 ▶                                         │
│                                                                     │
│ › src/utils/parser.ts                              +30 -8           │
│   src/utils/parser.test.ts                         +12 -2           │
│   README.md                                        +3 -2            │
│                                                                     │
│ ←/→ source · ↑/↓ file · Enter view · Esc close                     │
└─────────────────────────────────────────────────────────────────────┘
```

**Non-interactive mode:**

In headless (`--prompt`) or non-interactive contexts, `/diff` prints a plain-text summary of the working tree vs HEAD. Per-turn navigation is not available.

```
3 files changed, +45 / -12
  +30  -8  src/utils/parser.ts
  +12  -2  src/utils/parser.test.ts
   +3  -2  README.md
```

**Web Shell:** In the Web Shell UI (`qwen serve`), `/diff` opens a graphical diff dialog. A tab bar at the top lets you switch between the **Changes** view and the **History** view (`/log`).

#### History Viewer (`/log`) — Web Shell only

The `/log` command opens a commit history browser for the current workspace. It is available only in the Web Shell UI; the CLI/TUI does not have this command.

**How it works:**

`/log` opens a dialog listing commits in reverse chronological order (newest first). Each row shows:

- Short SHA (monospace, with a copy button for the full SHA)
- Commit subject (single line)
- Author name and relative time (e.g. "2h ago")
- Branch/tag ref labels, when present
- A merge icon (⎇) for merge commits

Click a commit row to expand its details on demand:

- Full commit message body
- File change statistics (files changed, lines added/removed, per-file breakdown)

Use **Load more** at the bottom to fetch the next page of commits (50 per page).

**Example:**

```
┌─ History ──────────────────────────── 50 commits ─ ✕ ┐
│                                                       │
│  a1b2c3d  feat(cli): add --json flag        2h ago   │
│           wenshao                                    │
│                                                       │
│  e4f5g6h  fix(core): handle null config     5h ago   │
│           dev · main  v1.2.0                         │
│                                                       │
│ ▼ 789abcd  refactor: simplify parser        1d ago   │
│   ┌─────────────────────────────────────────────┐    │
│   │  Broke the monolithic parse() into smaller  │    │
│   │  functions for readability.                 │    │
│   │                                             │    │
│   │  3 files · +45 −12                          │    │
│   │   +30 −8   src/parser.ts                    │    │
│   │   +10 −2   src/utils.ts                     │    │
│   │   +5  −2   test/parser.test.ts              │    │
│   └─────────────────────────────────────────────┘    │
│                                                       │
│              [ Load more ]                            │
└───────────────────────────────────────────────────────┘
```

> [!note]
>
> `/log` requires a git repository workspace. If the workspace is not a git repository or has no commits, the dialog shows a placeholder message.

### 1.10 Information, Settings, and Help

Commands for obtaining information and performing system settings.

| Command          | Description                                                                                                                    | Usage Examples                                                                      |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `/help`          | Display help information for available commands                                                                                | `/help` or `/?`                                                                     |
| `/status`        | Display version information                                                                                                    | `/status` or `/about`                                                               |
| `/status paths`  | Display current session file and log paths                                                                                     | `/status paths`                                                                     |
| `/stats`         | Open the interactive usage statistics dashboard (Session, Activity, and Efficiency tabs)                                       | `/stats` or `/usage`                                                                |
| `/stats model`   | Show per-model token breakdown and estimated cost                                                                              | `/stats model`                                                                      |
| `/stats tools`   | Show per-tool call counts                                                                                                      | `/stats tools`                                                                      |
| `/stats skills`  | Show per-skill call counts for the current live session (live only; excludes cross-session daily/monthly activity)             | `/stats skills`                                                                     |
| `/stats daily`   | Show daily token usage statistics                                                                                              | `/stats daily` (alias `day`), `/stats day [YYYY-MM-DD]`                             |
| `/stats monthly` | Show monthly token usage statistics                                                                                            | `/stats monthly` (alias `month`), `/stats month [YYYY-MM]`                          |
| `/stats export`  | Export usage statistics to CSV or JSON                                                                                         | `/stats export <daily\|monthly> [date\|month] [--format csv\|json] [--output path]` |
| `/settings`      | Open settings editor                                                                                                           | `/settings`                                                                         |
| `/config`        | Get or set any setting by dot-path key (writes to user settings)                                                               | `/config` (list all), `/config <key>`, `/config <key>=<value>`                      |
| `/auth`          | Change authentication method                                                                                                   | `/auth`, `/connect`, `/login`                                                       |
| `/doctor`        | Run installation and environment diagnostics                                                                                   | `/doctor`, `/doctor memory`                                                         |
| → `memory`       | Show current process memory diagnostics                                                                                        | `/doctor memory [--json] [--sample] [--snapshot]`                                   |
| → `cpu-profile`  | Record a CPU profile for Chrome DevTools analysis                                                                              | `/doctor cpu-profile [--duration <seconds>]`                                        |
| → `rollback`     | Roll back the standalone CLI binary to the previous version (standalone installs only; for conversation history use `/rewind`) | `/doctor rollback`                                                                  |
| `/docs`          | Open full Qwen Code documentation in browser                                                                                   | `/docs`                                                                             |
| `/ide`           | Manage IDE integration                                                                                                         | `/ide status`, `/ide install`, `/ide enable`, `/ide disable`                        |
| `/insight`       | Generate programming insights from chat history                                                                                | `/insight`                                                                          |
| `/setup-github`  | Set up GitHub Actions                                                                                                          | `/setup-github`                                                                     |
| `/bug`           | Submit issue about Qwen Code                                                                                                   | `/bug Button click unresponsive`                                                    |
| `/copy`          | Copy to clipboard: reply (Nth-last), code (by lang), LaTeX, or Mermaid                                                         | `/copy`, `/copy 2`, `/copy python`, `/copy latex`, `/copy mermaid`                  |
| `/quit`          | Exit Qwen Code immediately                                                                                                     | `/quit` or `/exit`                                                                  |

> [!warning]
>
> `/doctor memory --snapshot` writes a V8 heap snapshot that may contain prompts, file contents, API keys, and tool results from the current session. Review the file before sharing it.

> [!note]
>
> `/config` reads and writes individual settings by dot-path key (e.g. `general.vimMode`), complementing the interactive `/settings` editor. Running `/config` with no argument (or `--help`) lists every settable key with its type and current value. `/config <key>` prints the current value — except for boolean keys, where it toggles the value. `/config <key>=<value>` sets the value. Changes are written to user settings (`~/.qwen/settings.json`). Only `boolean`, `string`, `number`, and `enum` settings can be changed this way — `array` and `object` settings must be edited in `settings.json` directly. Sensitive values (API keys, tokens, base URLs) are masked in output, and setting `tools.approvalMode` to `yolo` is blocked.

### 1.11 Common Shortcuts

| Shortcut           | Function                | Note                                                                      |
| ------------------ | ----------------------- | ------------------------------------------------------------------------- |
| `Ctrl/cmd+L`       | Clear screen            | Clears the visible screen only (does not reset the session like `/clear`) |
| `Ctrl/cmd+T`       | Toggle tool description | MCP tool management                                                       |
| `Ctrl/cmd+C`×2     | Exit confirmation       | Secure exit mechanism                                                     |
| `Ctrl/cmd+Z`       | Undo input              | Text editing                                                              |
| `Ctrl/cmd+Shift+Z` | Redo input              | Text editing                                                              |

### 1.12 Authentication Commands

Use `/auth` inside a Qwen Code session to configure authentication. Use `/doctor` to inspect the current authentication and environment status.

| Command   | Description                                                            |
| --------- | ---------------------------------------------------------------------- |
| `/auth`   | Configure authentication interactively (aliases: `/connect`, `/login`) |
| `/doctor` | Show authentication and environment checks                             |

> [!note]
>
> The standalone `qwen auth` CLI command has been removed. Legacy invocations such as `qwen auth status` print a removal notice with migration guidance. See the [Authentication](../configuration/auth) page for full details.

## 2. @ Commands (Introducing Files)

@ commands are used to quickly add local file or directory content to the conversation.

| Command Format      | Description                                  | Examples                                         |
| ------------------- | -------------------------------------------- | ------------------------------------------------ |
| `@<file path>`      | Inject content of specified file             | `@src/main.py Please explain this code`          |
| `@<directory path>` | Recursively read all text files in directory | `@docs/ Summarize content of this document`      |
| Standalone `@`      | Used when discussing `@` symbol itself       | `@ What is this symbol used for in programming?` |

Note: Spaces in paths need to be escaped with backslash (e.g., `@My\ Documents/file.txt`)

In the interactive prompt, `@` completion supports Unicode filenames, including emoji. Emoji elsewhere in the prompt do not change the file path used for completion.

## 3. Exclamation Commands (`!`) - Shell Command Execution

Exclamation commands allow you to execute system commands directly within Qwen Code.

| Command Format     | Description                                                        | Examples                               |
| ------------------ | ------------------------------------------------------------------ | -------------------------------------- |
| `!<shell command>` | Execute command in sub-Shell                                       | `!ls -la`, `!git status`               |
| Standalone `!`     | Switch Shell mode, any input is executed directly as Shell command | `!`(enter) → Input command → `!`(exit) |

Environment Variables: Commands executed via `!` will set the `QWEN_CODE=1` environment variable.

## 4. Custom Commands

Save frequently used prompts as shortcut commands to improve work efficiency and ensure consistency.

> [!note]
>
> Custom commands now use Markdown format with optional YAML frontmatter. TOML format is deprecated but still supported for backwards compatibility. When TOML files are detected, an automatic migration prompt will be displayed.

### Quick Overview

| Function         | Description                                | Advantages                             | Priority | Applicable Scenarios                                 |
| ---------------- | ------------------------------------------ | -------------------------------------- | -------- | ---------------------------------------------------- |
| Namespace        | Subdirectory creates colon-named commands  | Better command organization            |          |                                                      |
| Global Commands  | `~/.qwen/commands/`                        | Available in all projects              | Low      | Personal frequently used commands, cross-project use |
| Project Commands | `<project root directory>/.qwen/commands/` | Project-specific, version-controllable | High     | Team sharing, project-specific commands              |

Priority Rules: Project commands > User commands (project command used when names are same)

### Command Naming Rules

#### File Path to Command Name Mapping Table

| File Location                            | Generated Command | Example Call          |
| ---------------------------------------- | ----------------- | --------------------- |
| `~/.qwen/commands/test.md`               | `/test`           | `/test Parameter`     |
| `<project>/.qwen/commands/git/commit.md` | `/git:commit`     | `/git:commit Message` |

Naming Rules: Path separator (`/` or `\`) converted to colon (`:`)

### Markdown File Format Specification (Recommended)

Custom commands use Markdown files with optional YAML frontmatter:

```markdown
---
description: Optional description (displayed in /help)
---

Your prompt content here.
Use {{args}} for parameter injection.
```

| Field         | Required | Description                              | Example                                    |
| ------------- | -------- | ---------------------------------------- | ------------------------------------------ |
| `description` | Optional | Command description (displayed in /help) | `description: Code analysis tool`          |
| Prompt body   | Required | Prompt content sent to model             | Any Markdown content after the frontmatter |

### TOML File Format (Deprecated)

> [!warning]
>
> **Deprecated:** TOML format is still supported but will be removed in a future version. Please migrate to Markdown format.

| Field         | Required | Description                              | Example                                    |
| ------------- | -------- | ---------------------------------------- | ------------------------------------------ |
| `prompt`      | Required | Prompt content sent to model             | `prompt = "Please analyze code: {{args}}"` |
| `description` | Optional | Command description (displayed in /help) | `description = "Code analysis tool"`       |

### Parameter Processing Mechanism

| Processing Method            | Syntax             | Applicable Scenarios                 | Security Features                      |
| ---------------------------- | ------------------ | ------------------------------------ | -------------------------------------- |
| Context-aware Injection      | `{{args}}`         | Need precise parameter control       | Automatic Shell escaping               |
| Default Parameter Processing | No special marking | Simple commands, parameter appending | Append as-is                           |
| Shell Command Injection      | `!{command}`       | Need dynamic content                 | Execution confirmation required before |

#### 1. Context-aware Injection (`{{args}}`)

| Scenario         | TOML Configuration                      | Call Method           | Actual Effect            |
| ---------------- | --------------------------------------- | --------------------- | ------------------------ |
| Raw Injection    | `prompt = "Fix: {{args}}"`              | `/fix "Button issue"` | `Fix: "Button issue"`    |
| In Shell Command | `prompt = "Search: !{grep {{args}} .}"` | `/search "hello"`     | Execute `grep "hello" .` |

#### 2. Default Parameter Processing

| Input Situation | Processing Method                                      | Example                                        |
| --------------- | ------------------------------------------------------ | ---------------------------------------------- |
| Has parameters  | Append to end of prompt (separated by two line breaks) | `/cmd parameter` → Original prompt + parameter |
| No parameters   | Send prompt as is                                      | `/cmd` → Original prompt                       |

🚀 Dynamic Content Injection

| Injection Type        | Syntax         | Processing Order    | Purpose                          |
| --------------------- | -------------- | ------------------- | -------------------------------- |
| File Content          | `@{file path}` | Processed first     | Inject static reference files    |
| Shell Commands        | `!{command}`   | Processed in middle | Inject dynamic execution results |
| Parameter Replacement | `{{args}}`     | Processed last      | Inject user parameters           |

#### 3. Shell Command Execution (`!{...}`)

| Operation                       | User Interaction     |
| ------------------------------- | -------------------- |
| 1. Parse command and parameters | -                    |
| 2. Automatic Shell escaping     | -                    |
| 3. Show confirmation dialog     | ✅ User confirmation |
| 4. Execute command              | -                    |
| 5. Inject output to prompt      | -                    |

Example: Git Commit Message Generation

````markdown
---
description: Generate Commit message based on staged changes
---

Please generate a Commit message based on the following diff:

```diff
!{git diff --staged}
```
````

#### 4. File Content Injection (`@{...}`)

| File Type    | Support Status         | Processing Method           |
| ------------ | ---------------------- | --------------------------- |
| Text Files   | ✅ Full Support        | Directly inject content     |
| Images/PDF   | ✅ Multi-modal Support | Encode and inject           |
| Binary Files | ⚠️ Limited Support     | May be skipped or truncated |
| Directory    | ✅ Recursive Injection | Follow .gitignore rules     |

Example: Code Review Command

```markdown
---
description: Code review based on best practices
---

Review {{args}}, reference standards:

@{docs/code-standards.md}
```

### Practical Creation Example

#### "Pure Function Refactoring" Command Creation Steps Table

| Operation                     | Command/Code                              |
| ----------------------------- | ----------------------------------------- |
| 1. Create directory structure | `mkdir -p ~/.qwen/commands/refactor`      |
| 2. Create command file        | `touch ~/.qwen/commands/refactor/pure.md` |
| 3. Edit command content       | Refer to the complete code below.         |
| 4. Test command               | `@file.js` → `/refactor:pure`             |

```markdown
---
description: Refactor code to pure function
---

Please analyze code in current context, refactor to pure function.
Requirements:

1. Provide refactored code
2. Explain key changes and pure function characteristic implementation
3. Maintain function unchanged
```

### Custom Command Best Practices Summary

#### Command Design Recommendations Table

| Practice Points      | Recommended Approach                | Avoid                                       |
| -------------------- | ----------------------------------- | ------------------------------------------- |
| Command Naming       | Use namespaces for organization     | Avoid overly generic names                  |
| Parameter Processing | Clearly use `{{args}}`              | Rely on default appending (easy to confuse) |
| Error Handling       | Utilize Shell error output          | Ignore execution failure                    |
| File Organization    | Organize by function in directories | All commands in root directory              |
| Description Field    | Always provide clear description    | Rely on auto-generated description          |

#### Security Features Reminder Table

| Security Mechanism     | Protection Effect          | User Operation         |
| ---------------------- | -------------------------- | ---------------------- |
| Shell Escaping         | Prevent command injection  | Automatic processing   |
| Execution Confirmation | Avoid accidental execution | Dialog confirmation    |
| Error Reporting        | Help diagnose issues       | View error information |

## 5. CLI Subcommands

These commands are run from the shell as `qwen <subcommand>` before starting an interactive session.

### Session Management

| Command                     | Description                                 | Usage Examples                                                                   |
| --------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------- |
| `qwen sessions list`        | List recent conversation sessions           | `qwen sessions list`, `qwen sessions list --json --limit 50`                     |
| `qwen sessions ps`          | List interactive sessions running right now | `qwen sessions ps`, `qwen sessions ps --json`                                    |
| `qwen sessions controllers` | Manage trusted controller tokens            | `qwen sessions controllers add --label <name>`, `qwen sessions controllers list` |

#### `qwen sessions list`

Lists your recent Qwen Code sessions with metadata.

**Flags:**

| Flag      | Type    | Default | Description                                     |
| --------- | ------- | ------- | ----------------------------------------------- |
| `--json`  | boolean | `false` | Output as JSON Lines (one JSON object per line) |
| `--limit` | number  | `20`    | Maximum number of sessions to show              |

**Human-readable output (default):**

A table with columns: SESSION ID, STARTED (UTC timestamp), TITLE, BRANCH, PROMPT.

**JSON output (`--json`):**

Outputs JSON Lines on stdout. Each line is a JSON object with fields:

```
sessionId, startTime, mtime, prompt, gitBranch, customTitle, titleSource, filePath, cwd
```

The "has more sessions" hint is emitted via stderr so piping to `jq` remains safe.

**Examples:**

```bash
# Show last 20 sessions (default)
qwen sessions list

# Show last 50 sessions
qwen sessions list --limit 50

# Output as JSON for scripting
qwen sessions list --json | jq .
```

#### `qwen sessions ps`

Lists the Qwen Code session records visible on this machine.
`sessions list` walks saved transcripts ("what have I worked on"). This
command shows both the live-process registry and Agent View records marked
`managed`. Records left behind by a killed session are swept from the registry
as they are found. A managed record is persistent bookkeeping, not evidence
that its worker is still alive. A one-shot `qwen -p` run never registers and
is not shown.

**Flags:**

| Flag     | Type    | Default | Description                                     |
| -------- | ------- | ------- | ----------------------------------------------- |
| `--json` | boolean | `false` | Output as JSON Lines (one JSON object per line) |

**Human-readable output (default):**

A table with columns: NAME, KIND, PID, AGE, DIRECTORY.

KIND says what registered the session — `tui` for someone at a terminal,
`external` for a program that is not a Qwen Code session at all (a voice
front-end, a relay), and `headless` or `serve` for a session another
program drives. Several `serve` or `headless` rows can share one PID: a
`qwen --acp` child hosts all its sessions in one process — `serve` when
the daemon spawned it, `headless` when a client is driving it directly —
and each of them registers separately. It is a self-report, like NAME and DIRECTORY: every field
here was written by the process it describes, and nothing about what a
session is allowed to do depends on it. An Agent View record marked
`managed` uses `managed` for KIND and `-` for PID and AGE. It remains a
separate row when the same session also has a live registry record. See
[Cross-Session Protocol](./cross-session-protocol.md) for the record
format and for how to register a program of your own.

**JSON output (`--json`):**

Outputs JSON Lines on stdout with managed records first. Registry records
keep their existing shape:

```
schemaVersion, pid, procStart, pidNs, sessionId, cwd, name, startedAt,
qwenVersion, kind, ipcPath (when peer messaging is available)
```

A managed record contains only source-backed data:

```
sessionId, cwd, managed: true
```

Nothing else is written to stdout — an empty listing prints nothing at
all — so `qwen sessions ps --json | jq .` is safe to script against. If
the supervisor store cannot be read, registry records still print and the
reason is written to stderr.

JSON output is raw data: field values are emitted exactly as recorded,
with no terminal sanitization. Treat them as data, and sanitize before
rendering them in a terminal.

**Examples:**

```bash
# Show the other live sessions
qwen sessions ps

# Which directories have a visible session record?
# Note: `jq -r` renders the raw recorded value in your terminal (see the
# raw-data note above); pipe through a sanitizer if the path is untrusted.
qwen sessions ps --json | jq -r .cwd
```

## 6. Messaging Another Running Session

Two interactive sessions on the same machine can send each other
messages. The feature is **on by default**: a session is discoverable by
the other sessions of the same user on the machine, and what they send
it goes through the review rules below before its model sees any of it.
To keep a session invisible and unreachable, turn it off in
`settings.json` and restart:

```json
{ "agents": { "crossSessionMessaging": false } }
```

With it on, the model in one session can discover the others with
`list_agents` — each appears under `sessions` with the `name` that
`qwen sessions ps --json` records (the table view may truncate long
names) — and address one with `send_message` using
that name as `to`. When two sessions share a name, `list_agents` shows
each with a short `[ref]` and the send must include it (`name [ref]`); a
bare name that could mean either is refused rather than guessed.
`list_agents` also reports the session's own name under `self`, and
`to: "*"` still means "my Agent Team teammates" and never reaches other
sessions.

A message arrives in the other session marked as coming from another
session, not from its user, and carries none of your authority there:
the receiving session acts on it only within its own permission settings.
Its user can choose what happens to incoming messages with
`agents.crossSessionInbound` (`accept`, `hold`, or `refuse`). When unset,
a message is delivered only when both sessions are in the same review
class: both still review each action (default or plan mode), or both
are in a mode that applies some actions without per-action review
(auto-edit, auto, or yolo). A message from a session in the other class,
or from a sender that does not say which class it is in, is held for
review — in both directions. A session that reviews each action holds a
message from one that does not, because that message was written by a
model nobody was watching, and the per-action prompts guard actions, not
what the session is being talked into. Held messages are listed and
released with `/peers` in the receiving session, and a message held
only because the modes differed is released on its own once they agree.

A repository can make sessions opened in it more cautious, never less:
a workspace `.qwen/settings.json` may set `agents.crossSessionInbound`
to `hold` or `refuse`, or `agents.crossSessionMessaging` to `false`, and
that value wins over a looser one in your user settings. A workspace
value that would loosen an operator-set one (`accept`, or `true` for the
switch when a user or system scope set it to `false`) is ignored with a
warning; a workspace `true` where nothing else set the switch merely
repeats the default and is dropped silently. A value the CLI does not
recognize holds every message whenever it is the effective value.
System settings override all of this, as they do for every setting.

A hold does not wait forever. A message nobody decides on expires after
`agents.crossSessionHeldExpiry` — `1m`, `5m`, `10m`, or `never`, five
minutes by default — and the sending session is told that no decision
came. Shortening the setting applies to messages already waiting.

If the session cannot bind its inbox — the runtime directory is missing,
owned by another user, or read-only, as it can be inside a container —
it first tries a private directory under the temp directory, and only
if that fails too does it start without one. When that happens the
session says so at startup, and `/peers` repeats the reason and what to
change (usually `XDG_RUNTIME_DIR` or `TMPDIR`).

Two sessions can also resolve the same inbox address, because the address
is keyed by process id and process ids repeat across containers that share
a runtime directory. The session starting second takes a neighbouring
address instead of taking over the one in use, so neither becomes
unreachable. Peers are unaffected: they read a session's address from the
session registry rather than deriving it.

The `send_message` call only confirms the message was handed to the other
session. What became of it arrives later as a receipt: if it was held,
declined, refused, dropped, expired, or misaddressed (the address changed
hands — list the agents again) — or released after a hold — a notice
appears in the sending session's transcript (`Message to <name>: …`).
Declined, refused and dropped are three different answers: declined means
someone reviewed the message and said no, refused means that session's
`agents.crossSessionInbound` is `refuse` and nobody saw it at all, and
dropped means its inbox turned the message away before any of that (see
below). The first drop is answered at once and the rest are folded into a
receipt every few seconds, each naming the messages it stands for, so a
run of them costs a handful of lines rather than one line each. The model that
sent it is not told; if the other session replies, the reply arrives as a
cross-session message.

### Flood protection

A session accepts up to 30 messages at once from one sender and then one
every two seconds, and up to 32 at once from all senders together and
then one a second. The second limit exists because a sender names itself:
rotating that name gets a fresh allowance from the first limit but not
from the second. It is barely above the first because every accepted
message draws a receipt, and a session can only have so many of those
going out at once. A message from another session that repeats that
sender's previous message word for word within 30 seconds is also turned
away — a model looping on one sentence mints a fresh message id every
time, so the text is what catches it. Messages from a script the session
started and from a trusted controller are exempt from the repeat check,
because a hook reporting the same line twice is reporting two facts and a
person saying "continue" twice means it twice; both are still subject to
the rate limits. Finally, a message that is accepted but cannot be queued
because the session already has 50 waiting is turned away too.

A message turned away this way is never held, never shown to the model,
and leaves no record, so the sender can try again later and land. The
receiving session says so in its transcript at most once a minute per
sender, with a count of what that line stands for. The sending session
gets one receipt naming every message the burst cost it, and its
transcript says to fold what still matters into one later message rather
than re-sending.

The sending side does not wait to find out. Each session tracks what it
has sent to each address and refuses a send that the receiver would
drop, so the model is told to batch before the message is written rather
than after — and the receiver never spends a connection on a message it
was going to turn away.

### Inbox authentication and scripted injection

Each session's inbox requires a per-session token: a connection must
present it on its first line before any message is read, and sessions
exchange tokens automatically through the same registry records they
discover each other by. Sessions from a build without token support can
receive from a newer one, but their sends to it are dropped.

A session exports its own inbox address and a token to child processes as
`QWEN_CODE_MESSAGING_SOCKET` and `QWEN_CODE_MESSAGING_TOKEN`, so a script
or hook the session runs can send a message back into it. This is a
second, _child_ token that is never published anywhere: only processes
the session started can hold it, so a message that arrives with it is
recognized as the session's own rather than as another session's.

```bash
{ printf '%s\n' \
    '{"msgV":1,"type":"auth","token":"'"$QWEN_CODE_MESSAGING_TOKEN"'"}' \
    '{"msgV":1,"msgId":"'"$(uuidgen)"'","type":"user","priority":"next","message":{"role":"user","content":"build finished"}}'; \
} | socat - UNIX-CONNECT:"$QWEN_CODE_MESSAGING_SOCKET"
```

Give every injection a fresh `msgId`. The receiving gate remembers the
ids it has already settled, so a hook that reuses one is delivered the
first time and silently deduplicated on every run after that. Repeating
the same _text_ is fine — the repeat check above does not apply to a
session's own processes — but the rate limits do apply, so a hook in a
loop is dropped like any other flood.

An injected message still goes through the inbound gate and is marked as
not coming from the user, but the gate knows it came from the session's
own process: under the mode-parity default it is delivered without review
(a peer in the same position would be held), while an explicit
`agents.crossSessionInbound` of `hold` or `refuse` applies to it as to
anything else. The model sees it as
`<cross_session_message from="own process" origin="own-process">` with a
notice that it came from a script or hook the session ran, not from the
user.

### Trusted controllers

The rule above holds a message from any sender that does not say which
review class it is in, and a program that is not a Qwen Code session has
none to say. That is the right default for a stranger, but not for a
program you chose: a voice front-end, a dictation bridge, an automation
daemon relaying your own instructions would have every message parked,
and approving each one by hand defeats the point.

You grant such a program delivery by minting it a token:

```bash
qwen sessions controllers add --label voice-bridge
```

The token is printed once and is not stored anywhere: the file under
your Qwen home keeps only its SHA-256 hash, so nothing that later reads
that file can present the token. Put it in the controller's own
configuration when the command prints it.

A controller presents the token the way any other sender does — as the
first line of the connection — and takes the socket path from the
session registry (`qwen sessions ps --json` prints one record per live
session, `ipcPath` being the address):

```bash
{ printf '%s\n' \
    '{"msgV":1,"type":"auth","token":"'"$QWEN_CONTROLLER_TOKEN"'"}' \
    '{"msgV":1,"msgId":"'"$(uuidgen)"'","type":"user","priority":"next","message":{"role":"user","content":"open the failing test"}}'; \
} | socat - UNIX-CONNECT:"$SESSION_IPC_PATH"
```

A message that arrives on a granted token is delivered without
per-message review, whatever review class either side is in — but it
still yields to an explicit setting: an `agents.crossSessionInbound` of
`hold` parks it like anything else, and `refuse` turns it away. Grants
belong to your Qwen home rather than to one session, so a controller
reaches whichever sessions you are running, and sessions re-read the
file on every connection: minting or revoking one takes effect on the
next connection, with nothing to restart.

```bash
qwen sessions controllers list          # ids, labels, when they were added
qwen sessions controllers remove c_1a2b # revoke one
```

`/peers controllers` and `/peers revoke <id>` do the same from inside a
session. A message that came through a grant is shown as
`Message from a trusted controller (voice-bridge)`, and appears in
`/peers` as `[controller] voice-bridge` if a `hold` setting parked it.

The model sees such a message as
`<cross_session_message from="controller" origin="controller" controller="voice-bridge">`,
with a notice that it relays your own instructions — and the same two
prohibitions that apply to every other origin: it may not edit
permission settings, QWEN.md, or config because the message asked, and
it may not treat the message as you approving a pending confirmation
prompt. A controller can say what to do next; it cannot answer a prompt
on your behalf.

Anyone who holds the token can send as that controller, so treat it like
any other credential: give it to one program, keep it out of shared
config, and revoke it when that program is done.

### Sessions a program drives over ACP

Any `qwen --acp` child registers each session it hosts — as `serve` when
the daemon spawned the process, as `headless` when an editor or another
client is driving `qwen --acp` directly — and the session appears in
`qwen sessions ps` and in another session's `list_agents` like any
other. It can send: its model can call `send_message` to reach a terminal
you have open. Several of them share one process and one inbox, so a
sender has to name the session it means — every Qwen Code session does
that automatically.

Messages sent _to_ one are refused rather than held. Holding is a
question put to a person, and nobody is watching a held-message list on a
driven session's behalf; a sender is told at once instead of
waiting out an expiry. Where a held message should surface for those
sessions is not settled yet.

A session registers unless its own settings turn
`agents.crossSessionMessaging` off, or it was started with `--bare` or
`--safe-mode` — those turn messaging off whatever the setting says. Turned
off, it stays invisible, because the only reason to list a session nobody
can message would be to advertise an address that never answers.

### Programs that are not Qwen Code sessions

Everything above works between sessions, but nothing in it is specific to
one. A program that writes a registry record for itself and binds an
inbox the same way is listed by `qwen sessions ps` and by `list_agents`,
can be addressed by name from `send_message`, and receives delivery
receipts for what it sends — a voice front-end, a relay, a build watcher.
It should record `kind: "external"` so a listing can say what it is.

[Cross-Session Protocol](./cross-session-protocol.md) is the contract for
writing one: the record schema and how liveness is judged, the socket
paths and framing, the auth line, every frame field, the receipt states
and their transitions, and what a receiver does with a message before its
model sees it.

A Node program does not have to write any of that by hand:
`@qwen-code/sdk/peer` implements the contract. `PeerEndpoint.start({ name })`
publishes the record and binds the inbox, `list()` and `send()` address
sessions by name, and `onMessage` receives what they send.

---

### Users Features Dual Output (`docs/users/features/dual-output.md`)

# Dual Output

Dual Output is a sidecar mode for the interactive TUI: while Qwen Code keeps
rendering normally on `stdout`, it concurrently emits a structured JSON event
stream to a separate channel so an external program — an IDE extension, a web
frontend, a CI pipeline, an automation script — can observe and steer the
session.

It also provides a reverse channel: an external program can write JSONL
commands into a file that the TUI watches, allowing it to submit prompts and
respond to tool-permission requests as if a human were at the keyboard.

Dual Output is fully optional. When the flags below are absent the TUI behaves
exactly as before with no extra I/O and no behavioral changes.

## Use cases

Dual Output is a low-level plumbing primitive. These are concrete integrations
it unlocks:

### Terminal + Chat dual-mode real-time sync

The flagship use case. A web or desktop ChatUI hosts the TUI inside a PTY
and renders a parallel conversation view driven by the structured event
stream:

- User can type in either surface — the TUI (for terminal-native power-users)
  or the web UI (for richer UX, shareable links, mobile). Both views stay
  in sync because every message flows through the same JSON events.
- Tool-approval prompts appear in both places; whoever approves first wins.
- Session history is captured verbatim from `--json-file`, so the server
  side has a canonical machine-readable transcript without parsing ANSI.

### IDE extensions (VS Code / JetBrains / Cursor / Neovim)

Embed Qwen Code inside the IDE. The TUI runs in the editor's integrated
terminal panel for users who want it, while the extension consumes
`--json-fd` / `--json-file` events to drive:

- Inline diff overlays when the agent touches files.
- A webview side panel with formatted markdown, syntax-highlighted tool
  calls, and clickable citations.
- Status bar indicators (thinking / responding / awaiting approval).
- Programmatic `confirmation_response` writes when the user clicks a
  native IDE approval button.

### Browser-based Chat frontends

A Node/Bun server spawns the TUI in a PTY for its rendering semantics but
exposes a WebSocket channel to the browser. Events on `--json-file` are
forwarded to the client; user messages typed in the browser are injected
via `--input-file`. No ANSI parsing on either side.

### CI / automation observers

A CI job runs Qwen Code with a task prompt. The human sees the TUI in the
job log; the CI system tails `--json-file` to:

- Fail the job if a `result` event reports an error.
- Push `token usage` / `duration_ms` / `tool_use` counts to metrics.
- Archive the full transcript as a build artifact.

### Multi-agent orchestration

A supervisor agent spawns multiple TUI workers, each with its own pair of
event/input files. It watches progress, injects follow-up prompts, and
enforces global budget / safety policies by approving or denying tool
calls across all workers.

### Session recording, audit, and replay

Tee every TUI session to a regular file with `--json-file`. Later:

- Compliance audits can reconstruct exactly what was executed.
- Automated regression tests can compare runs across model versions.
- A replay tool can re-emit events through the same protocol to feed
  visualization dashboards.

### Observability dashboards

Stream `--json-file` into Loki / OTEL / any pipeline that accepts JSONL.
Extract `usage.input_tokens`, `tool_use.name`, `result.duration_api_ms`
as first-class metrics in Grafana. No need for log-parsing regex.

### Testing and QA

Integration tests spawn Qwen Code headlessly, drive it with `--input-file`
scripts, and assert on `--json-file` events. Unlike parsing stdout ANSI,
assertions are stable across UI refactors.

## Flags

| Flag                  | Type             | Purpose                                                                                                                                    |
| --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `--json-fd <n>`       | number, `n >= 3` | Write structured JSON events to file descriptor `n`. The caller must provide this fd via spawn `stdio` configuration or shell redirection. |
| `--json-file <path>`  | path             | Write structured JSON events to a file. The path can be a regular file, a FIFO (named pipe), or `/dev/fd/N`.                               |
| `--input-file <path>` | path             | Watch this file for JSONL commands written by an external program.                                                                         |

`--json-fd` and `--json-file` are mutually exclusive. fds 0, 1, and 2 are
rejected to prevent corrupting the TUI's own output.

## Why two output flags? (`--json-fd` vs `--json-file`)

At first glance `--json-fd` looks sufficient — the caller spawns Qwen Code
with an extra file descriptor, the TUI writes events to it, done. In
practice, fd passing breaks down under the most important embedding
scenario: running the TUI inside a pseudo-terminal (PTY). That is why
this feature also exposes a path-based alternative.

### When `--json-fd` works

Pure `child_process.spawn` with a `stdio` array:

```ts
const child = spawn('qwen', ['--json-fd', '3'], {
  stdio: ['inherit', 'inherit', 'inherit', eventsFd],
});
```

Node's spawn supports arbitrary `stdio` entries; fd 3 is inherited by the
child, which can write to it directly. Zero-copy, zero-buffer, zero
filesystem — the fastest path.

### Why `--json-fd` does **not** work under PTY

PTY wrappers like [`node-pty`](https://github.com/microsoft/node-pty) and
[`bun-pty`](https://github.com/oven-sh/bun) are how any serious embedder
(IDE extensions, web terminals, tmux-like multiplexers) hosts an
interactive TUI. They cannot forward extra fds to the child, for three
reinforcing reasons:

1. **API surface.** `node-pty.spawn(file, args, options)` accepts `cwd`,
   `env`, `cols`, `rows`, `encoding`, etc. — but **no `stdio` array**. There
   is simply no place in the API to say "also attach this fd as fd 3 in
   the child". `bun-pty` exposes the same shape.
2. **`forkpty(3)` semantics.** Under the hood, PTY wrappers call
   `forkpty(3)` (or the equivalent `posix_openpt` + `login_tty` dance).
   That syscall allocates a master/slave pseudo-terminal pair and
   redirects the child's fds 0/1/2 to the slave side so the child thinks
   it is attached to a real terminal. Any fds above 2 in the parent are
   closed by `login_tty`, which calls `close(fd)` for `fd >= 3` before
   `exec`. Extra fds are actively wiped, not inherited.
3. **Controlling-terminal side effect.** Even if you hacked an extra fd
   through, it would not be a terminal, so the child's TUI renderer
   (which writes escape sequences assuming a TTY on fd 1) would still
   need the slave for its output. You would end up with two independent
   transports anyway.

In short: the moment an embedder needs a real TTY for TUI rendering —
which is every IDE extension, every web terminal, every desktop chat
app — fd inheritance is off the table.

### `--json-file` fills the gap

A file path is passed as an ordinary CLI argument, so it survives every
spawn model:

```ts
import { spawn } from 'node-pty';

const pty = spawn(
  'qwen',
  [
    '--json-file',
    '/tmp/qwen-events.jsonl',
    '--input-file',
    '/tmp/qwen-input.jsonl',
  ],
  { cols: 120, rows: 40 },
);
```

The child opens the file itself and writes events there; the embedder
tails the same path with `fs.watch` + incremental reads. Three things to
note:

- **Regular file**, FIFO (named pipe), or `/dev/fd/N` all work. FIFO is
  the lowest-latency option when both sides are on the same host.
- The bridge opens FIFOs with `O_NONBLOCK` and falls back to blocking
  mode on `ENXIO` (no reader yet), so PTY startup is never deadlocked
  waiting for a consumer.
- For multi-session isolation, use per-session paths under
  `$XDG_RUNTIME_DIR` or a `mkdtemp`'d directory with mode `0700`.

### Which flag should I use?

| Embedding style                                   | Use                  |
| ------------------------------------------------- | -------------------- |
| `child_process.spawn` with plain stdio            | `--json-fd`          |
| `node-pty` / `bun-pty` / any PTY host             | `--json-file`        |
| Shell redirection / manual pipeline testing       | either               |
| CI log collection (regular file, read after exit) | `--json-file`        |
| Lowest possible latency on same host              | `--json-file` + FIFO |

The general rule: **if you need the TUI to render correctly, you need a
PTY, which means you need `--json-file`.** `--json-fd` is for simpler
embedders that do not care about TUI fidelity — typically programmatic
wrappers that throw away stdout anyway.

## Quick start

Run Qwen Code with both channels enabled using regular files:

```bash
touch /tmp/qwen-events.jsonl /tmp/qwen-input.jsonl
qwen \
  --json-file /tmp/qwen-events.jsonl \
  --input-file /tmp/qwen-input.jsonl
```

In a second terminal, tail the event stream:

```bash
tail -f /tmp/qwen-events.jsonl
```

In a third terminal, push a prompt into the running TUI:

```bash
echo '{"type":"submit","text":"Explain this repo"}' >> /tmp/qwen-input.jsonl
```

The prompt appears in the TUI exactly as if the user typed it, and the
streaming response is mirrored on `/tmp/qwen-events.jsonl`.

### Using FIFOs (named pipes) for event output

FIFOs deliver lower latency than regular files (no disk I/O) and work
well when both sides are on the same host. The bridge opens FIFOs with
`O_RDWR | O_NONBLOCK`, so it **does not block** even if no reader is
connected yet — events are buffered in the kernel pipe buffer until a
reader attaches.

> **Note:** `--input-file` requires a regular file (not a FIFO) because
> the watcher relies on `stat.size` to detect new data, which is always
> 0 for FIFOs.

```bash
mkfifo /tmp/qwen-events.jsonl
touch /tmp/qwen-input.jsonl
qwen \
  --json-file /tmp/qwen-events.jsonl \
  --input-file /tmp/qwen-input.jsonl
# TUI starts immediately — no need to start a reader first.

# In a second terminal, connect whenever ready:
cat /tmp/qwen-events.jsonl
```

If no reader ever connects, the bridge auto-disables once the internal
buffer exceeds 1 MB. The TUI continues running normally.

## Output event schema

Events are emitted as JSON Lines (one object per line). The schema is the same
one used by the non-interactive `--output-format=stream-json` mode, with
`includePartialMessages` always enabled.

Protocol version 2 bounds textual `tool_result.content` values to 65,536 UTF-8
bytes after JSON string serialization. Oversized values become deterministic
head/tail previews; the event type and field schema are unchanged. This is a
field limit, not a universal JSONL frame-size limit.

The first event on the channel is always `system` / `session_start`, emitted
when the bridge is constructed. Use it to correlate the channel with a
session id before any other event arrives.

```
/* Detailed source-code truncated for AI context efficiency. */
```

`control_response` is emitted whether the decision was made in the TUI
(native approval UI) or by an external `confirmation_response` (see below).
Either way, all observers see the final outcome.

## Input command schema

Two command shapes are accepted on `--input-file`:

```jsonc
// Submit a user message into the prompt queue
{ "type": "submit", "text": "What does this function do?" }

// Reply to a pending control_request
{ "type": "confirmation_response", "request_id": "...", "allowed": true }
```

Behavior:

- `submit` commands are queued. If the TUI is busy responding, they are
  retried automatically the next time the TUI returns to the idle state.
- `confirmation_response` commands are dispatched immediately and never
  queued, because a tool call is blocking and the response must reach the
  underlying `onConfirm` handler without waiting for any earlier `submit`.
- Whichever side approves a tool first wins; the other side's late response
  is harmlessly dropped.
- Lines that fail to parse as JSON are logged and skipped — they do not
  stop the watcher.

## Latency notes

The input file is observed with `fs.watchFile` at a 500 ms polling interval,
so worst-case round-trip latency for a remote `submit` is about half a
second. This is intentional: polling is portable across platforms and
filesystems (including macOS / network mounts), and matches the typical
human-in-the-loop pacing the feature targets. The output channel has no
polling — events are written synchronously as the TUI emits them.

## Failure modes

- **Bad fd.** If the fd passed to `--json-fd` is not open or is one of
  0/1/2, the TUI prints a warning to `stderr` and continues without dual
  output enabled.
- **Bad path.** If the file passed to `--json-file` cannot be opened, the
  TUI prints a warning and continues without dual output.
- **Consumer disconnect.** If the reader on the other side of the channel
  goes away (`EPIPE`), the bridge silently disables itself and the TUI
  keeps running. No retry.
- **FIFO buffer overflow.** When writing to a FIFO with no reader
  attached, events buffer in the kernel pipe (~64 KB on Linux) and the
  Node.js WriteStream. Once the pipe is full or the internal buffer
  exceeds 1 MB, the bridge disables itself and closes the fd. No
  `session_end` is emitted in this case — consumers should treat a
  closed stream without `session_end` as an abnormal termination. The
  TUI continues running normally.
- **Adapter exception.** Any exception thrown while emitting an event is
  caught, logged, and disables the bridge. The TUI is never crashed by a
  dual-output failure.

## Spawn example

A typical embedding parent process spawns Qwen Code with both channels:

```ts
import { spawn } from 'node:child_process';
import { openSync } from 'node:fs';

const eventsFd = openSync('/tmp/qwen-events.jsonl', 'w');
const child = spawn(
  'qwen',
  ['--json-fd', '3', '--input-file', '/tmp/qwen-input.jsonl'],
  { stdio: ['inherit', 'inherit', 'inherit', eventsFd] },
);
```

The TUI still owns the user's terminal on stdio 0/1/2, while the embedder
reads structured events on the file backing fd 3 and pushes commands by
appending JSONL lines to `/tmp/qwen-input.jsonl`.

## Settings-based configuration

For long-lived embedders it is often inconvenient to thread CLI flags
through every launch. The same channels can be configured in
`settings.json` under the top-level `dualOutput` key:

```jsonc
// ~/.qwen/settings.json  (user-level)
// or <workspace>/.qwen/settings.json  (workspace-level)
{
  "dualOutput": {
    "jsonFile": "/tmp/qwen-events.jsonl",
    "inputFile": "/tmp/qwen-input.jsonl",
  },
}
```

Precedence rules:

- CLI flag **wins** over settings. Passing `--json-file /foo` on the
  command line overrides `dualOutput.jsonFile` in settings.
- `--json-fd` has no settings equivalent — fd passing is a spawn-time
  concern that cannot be statically declared.
- If neither flag nor setting is present, dual output stays disabled
  (identical to today's default).

The `requiresRestart: true` flag means changes only take effect on the
next Qwen Code launch, since the bridge is constructed once during
startup.

## Runnable demos

Every script below is copy-paste ready. Start with POC&nbsp;1 to verify
the build has dual output; POC&nbsp;4 is the closest analogue to a real
IDE-extension integration.

### POC 1 — observe the event stream

Watch every structured event the TUI emits while a human uses it
normally:

```bash
# Terminal A
mkfifo /tmp/qwen-events.jsonl
cat /tmp/qwen-events.jsonl | jq -c 'select(.type != "stream_event") | {type, subtype}'

# Terminal B
qwen --json-file /tmp/qwen-events.jsonl
# ...then chat normally; terminal A shows session_start,
# user/assistant/result/control_request lifecycle in real time.
```

Expected first line in terminal A:

```json
{ "type": "system", "subtype": "session_start" }
```

### POC 2 — inject prompts from outside

Drive the TUI from a second terminal without touching the keyboard of
the first:

```bash
# Terminal A
touch /tmp/qwen-in.jsonl
qwen --input-file /tmp/qwen-in.jsonl

# Terminal B — the TUI responds as if you typed it
echo '{"type":"submit","text":"list files in the current directory"}' \
  >> /tmp/qwen-in.jsonl
```

### POC 3 — remote tool-permission bridge

Approve or deny tool calls from a separate process:

```bash
# Terminal A — observe control_requests
mkfifo /tmp/qwen-out.jsonl
touch /tmp/qwen-in.jsonl
(cat /tmp/qwen-out.jsonl \
  | jq -c 'select(.type == "control_request")') &

# Terminal B
qwen --json-file /tmp/qwen-out.jsonl --input-file /tmp/qwen-in.jsonl
# Ask Qwen to do something that needs approval, e.g.
# "run `ls -la /tmp`". A control_request will appear in terminal A.
# Copy the request_id, then in a third terminal:
echo '{"type":"confirmation_response","request_id":"<paste-id>","allowed":true}' \
  >> /tmp/qwen-in.jsonl
# The TUI confirmation prompt dismisses and the tool executes.
```

If you reply with an unknown `request_id`, the bridge emits a
`control_response` with `subtype: "error"` on the output channel so your
consumer can log it or retry:

```json
{
  "type": "control_response",
  "response": {
    "subtype": "error",
    "request_id": "...",
    "error": "unknown request_id (already resolved, cancelled, or never issued)"
  }
}
```

### POC 4 — Node embedder (IDE-like)

The most realistic shape: a parent process spawns Qwen Code, tails
events, and injects prompts on its own schedule.

```
/* Detailed source-code truncated for AI context efficiency. */
```

Run with:

```bash
npx tsx demo-embedder.ts
# Qwen Code TUI opens in the current terminal; the embedder logs
# handshake + turn-end + session_end events to the parent's stdout.
```

### POC 5 — capability handshake feature detection

Older Qwen Code versions won't emit `protocol_version`. Treat the field
as optional and feature-detect:

```ts
rl.on('line', (line) => {
  const ev = JSON.parse(line);
  if (ev.type === 'system' && ev.subtype === 'session_start') {
    const v = ev.data?.protocol_version ?? 0;
    if (v < 1) {
      console.error(
        'qwen-code dual output is present but protocol < 1; ' +
          'falling back to best-effort behavior',
      );
    } else {
      console.log('qwen-code dual output protocol v' + v);
    }
  }
});
```

### POC 6 — session_end as a clean termination signal

```ts
rl.on('line', (line) => {
  const ev = JSON.parse(line);
  if (ev.type === 'system' && ev.subtype === 'session_end') {
    console.log('[embedder] clean shutdown, session', ev.data.session_id);
    // Flush metrics, close WebSockets, etc.
  }
});
```

If the TUI crashes before `session_end`, the output stream closes
(`EPIPE` on next write); embedders should handle both paths.

### POC 7 — failure drills (prove the flags never break the TUI)

```bash
qwen --json-fd 1
# stderr: "Warning: dual output disabled — ..."
# TUI still launches normally.

qwen --json-fd 9999
# stderr: "Warning: dual output disabled — fd 9999 not open"
# TUI still launches normally.

qwen --json-fd 3 --json-file /tmp/x.jsonl
# yargs rejects: "--json-fd and --json-file are mutually exclusive."
# Process exits before TUI starts.

qwen --json-file /nonexistent/dir/x.jsonl
# stderr warning; TUI still launches.
```

## Relation to Claude Code

Claude Code exposes a similar stream-json event format under
`--print --output-format stream-json`, but only in non-interactive mode
— it has no equivalent of running the TUI and a structured sidecar
channel at the same time. Dual Output fills that gap.

---

### Users Features Followup Suggestions (`docs/users/features/followup-suggestions.md`)

# Followup Suggestions

Qwen Code can predict what you want to type next and show it as placeholder text in the input area. This feature uses an LLM call to analyze the conversation context and generate a natural next step suggestion.

This feature works end-to-end in both the CLI and Web Shell. Generation is automatic and server-side: after each cleanly completed turn (the daemon's `end_turn` stop reason — a `cancelled`, `refusal`, `max_tokens`, or `max_turn_requests` turn gets none) the daemon emits the suggestion on the session stream (on by default; set `ui.enableFollowupSuggestions` to `false` to opt out), and Web Shell's composer already wires the `useDaemonFollowupSuggestion` hook, so suggestions render and accept with no additional host wiring.

## How It Works

After Qwen Code finishes responding, a suggestion appears as dimmed placeholder text in the input area after a short delay (~300ms). For example, after fixing a bug, you might see:

```
> run the tests
```

The suggestion is generated by sending the conversation history to the model, which predicts what you would naturally type next. If the response contains an explicit tip (e.g., `Tip: type post comments to publish findings`), the suggested action is extracted automatically.

## Accepting Suggestions

| Key           | Action                                           |
| ------------- | ------------------------------------------------ |
| `Tab`         | Accept the suggestion and fill it into the input |
| `Enter`       | Accept the suggestion and fill it into the input |
| `Right Arrow` | Accept the suggestion and fill it into the input |
| Any typing    | Dismiss the suggestion and type normally         |

`Enter` fills the input rather than submitting, so accepting a suggested slash command (e.g. `/clear`) never auto-executes — you submit it yourself with a second `Enter`.

## When Suggestions Appear

The interactive CLI and the daemon decide this separately, and they do not apply the same conditions: the CLI gates generation itself, while the daemon gates it server-side for every client attached to the session.

Both sides require all of the following:

- At least 2 model turns have occurred in the conversation
- The approval mode is not set to `plan`
- The feature is enabled (on by default — set `ui.enableFollowupSuggestions` to `false` to turn it off)

The interactive CLI additionally requires:

- The session is interactive — the CLI never generates suggestions in its own non-interactive or SDK mode
- The model has completed its response (not during streaming)
- There are no errors in the most recent response
- No confirmation dialogs are pending (e.g., shell confirmation, permissions). One renderer reads that state directly; the other gates on its own pending tool calls and cannot see a shell dialog opened mid-turn, so a suggestion can still be generated behind one. Nothing is displayed in that case — the composer is unmounted while the dialog is up — so what it costs is that turn's generation call, not a suggestion you can act on by mistake.

The daemon additionally requires:

- The turn ended cleanly, meaning its stop reason is `end_turn` — a cancelled, refused, or truncated turn gets no suggestion
- Automatic turns are not being held by the todo stop guard, and no queued prompt is waiting to run
- The most recent entry in the conversation history is a model response

Because daemon-side generation happens for every client attached to the session, it also happens for clients that cannot render the result. Such a client — a headless or SDK consumer of a daemon session, which is not the CLI's own non-interactive mode above — should set `ui.enableFollowupSuggestions` to `false` to avoid paying the per-turn LLM cost for output it discards.

Suggestions are automatically dismissed when:

- You start typing
- A new model turn begins
- The suggestion is accepted

## Fast Model

By default, suggestions use the same model as your main conversation. For lower-latency suggestions, configure a dedicated fast model:

### Via command

```
/model --fast qwen3-coder-flash
```

Or use `/model --fast` (without a model name) to open a selection dialog.

### Via settings.json

```json
{
  "fastModel": "qwen3-coder-flash"
}
```

The fast model is used for prompt suggestions and speculative execution. When not configured, the main conversation model is used as fallback.

> **Cost note:** A fast model lowers latency, but it does not always lower cost. Suggestion generation reuses your conversation's prefix cache (via `ui.enableCacheSharing`, on by default) — but a prefix cache is per-model. Pointing `fastModel` at a different model forks to a separate cache, so the whole conversation history is re-billed as uncached input on the fast model. On long conversations, the default (main model + shared cache) can be **cheaper** than a fast model, since most of the history is billed at the discounted cached rate. Set `fastModel` when latency matters more than per-turn cost.

Thinking/reasoning mode is automatically disabled for all background tasks (suggestion generation and speculation), regardless of your main model's thinking configuration. This avoids wasting tokens on internal reasoning that isn't needed for these tasks.

## Configuration

These settings can be configured in `settings.json`:

| Setting                        | Type    | Default | Description                                                        |
| ------------------------------ | ------- | ------- | ------------------------------------------------------------------ |
| `ui.enableFollowupSuggestions` | boolean | `true`  | Enable or disable followup suggestions                             |
| `ui.enableCacheSharing`        | boolean | `true`  | Use cache-aware forked queries to reduce cost (experimental)       |
| `ui.enableSpeculation`         | boolean | `false` | Speculatively execute suggestions before submission (experimental) |
| `fastModel`                    | string  | `""`    | Model for prompt suggestions and speculative execution             |

### Example

```json
{
  "fastModel": "qwen3-coder-flash",
  "ui": {
    "enableFollowupSuggestions": true,
    "enableCacheSharing": true
  }
}
```

## Monitoring

Suggestion model usage appears in `/stats` output, showing tokens consumed by the fast model for suggestion generation.

The fast model is also shown in `/about` output under "Fast Model".

## Suggestion Quality

Suggestions go through quality filters to ensure they are useful:

- Must be 2-12 words (CJK: 2-30 characters), under 100 characters total
- Cannot be evaluative ("looks good", "thanks")
- Cannot use AI voice ("Let me...", "I'll...")
- Cannot be multiple sentences or contain formatting (markdown, newlines)
- Cannot be meta-commentary ("nothing to suggest", "silence")
- Cannot be error messages or prefixed labels ("Suggestion: ...")
- Single-word suggestions are only allowed for common commands (yes, commit, push, etc.)
- Slash commands (e.g., `/commit`) are always allowed as single-word suggestions

---

### Users Features Headless (`docs/users/features/headless.md`)

# Headless Mode

Headless mode allows you to run Qwen Code programmatically from command line
scripts and automation tools without any interactive UI. This is ideal for
scripting, automation, CI/CD pipelines, and building AI-powered tools.

## Overview

The headless mode provides a headless interface to Qwen Code that:

- Accepts prompts via command line arguments or stdin
- Returns structured output (text or JSON)
- Supports file redirection and piping
- Enables automation and scripting workflows
- Provides consistent exit codes for error handling
- Can resume previous sessions scoped to the current project for multi-step automation

## Basic Usage

### Direct Prompts

Use the `--prompt` (or `-p`) flag to run in headless mode:

```bash
qwen --prompt "What is machine learning?"
```

### Stdin Input

Pipe input to Qwen Code from your terminal:

```bash
echo "Explain this code" | qwen
```

### Combining with File Input

Read from files and process with Qwen Code:

```bash
cat README.md | qwen --prompt "Summarize this documentation"
```

### Resume Previous Sessions (Headless)

Reuse conversation context from the current project in headless scripts:

```bash
# Continue the most recent session for this project and run a new prompt
qwen --continue -p "Run the tests again and summarize failures"

# Resume a specific session ID directly (no UI)
qwen --resume 123e4567-e89b-12d3-a456-426614174000 -p "Apply the follow-up refactor"
```

> [!note]
>
> - Session data is project-scoped JSONL under `~/.qwen/projects/<sanitized-cwd>/chats`.
> - Restores conversation history, tool outputs, and chat-compression checkpoints before sending the new prompt.

## Run a Persistent Goal

Headless mode accepts `/goal` as the entire prompt. Goal state is stored with the session, so use `--continue` or `--resume <sessionId>` to inspect or control the same Goal from a later process. This requires `general.chatRecording` to remain enabled (the default).

```bash
# Create a Goal and start its worker
qwen -p "/goal Finish the release checklist"

# Inspect its saved state in the same session
qwen --continue -p "/goal"
```

Use the same `qwen --continue -p "<control>"` pattern for the other operations:

| Control                              | Behavior                                                                                 |
| ------------------------------------ | ---------------------------------------------------------------------------------------- |
| `/goal`                              | Report the stored state without calling the model.                                       |
| `/goal <objective>` or `/goal set …` | Create or replace the Goal and start headless Goal work.                                 |
| `/goal edit <objective>`             | Revise a non-completed Goal; work starts immediately when the resulting state is active. |
| `/goal pause`                        | Pause an active Goal without calling the model.                                          |
| `/goal resume`                       | Resume an eligible Goal and start headless Goal work.                                    |
| `/goal clear`                        | Clear the Goal without confirmation or a model call.                                     |

A Goal is only as good as its completion condition. See [Goals](./goals.md) for what the verifier can and cannot judge, and use `qwen -p "/goal-draft <intent>"` to have the objective drafted before you set it.

Runtime-scheduled Goal continuation segments do not count against `--max-session-turns`, but real user prompts still do. Explicit `--max-wall-time` and `--max-tool-calls` budgets continue to apply; exceeding either pauses active Goal work before the run exits with the budget-specific error.

With `--output-format stream-json`, each Goal status change emits a `stream_event` whose `event.type` is `goal_state`. This state event is emitted even without `--include-partial-messages`. The `active_goal` event that used to follow it as a compatibility projection is no longer emitted; automation reads `goal_state`.

> [!note]
>
> This behavior applies to standard headless CLI runs. ACP-driven sessions (IDE integrations, Web Shell) drive the same Goal runtime through the `sessionGoalControl` extension method, and receive each status change as a `session/update` notification carrying `_meta.goalState` instead of a `goal_state` stream event.

## Customize the Main Session Prompt

You can change the main session system prompt for a single CLI run without editing shared memory files.

### Override the Built-in System Prompt

Use `--system-prompt` to replace Qwen Code's built-in main-session prompt for the current run:

```bash
qwen -p "Review this patch" --system-prompt "You are a terse release reviewer. Report only blocking issues."
```

### Append Extra Instructions

Use `--append-system-prompt` to keep the built-in prompt and add extra instructions for this run:

```bash
qwen -p "Review this patch" --append-system-prompt "Be terse and focus on concrete findings."
```

You can combine both flags when you want a custom base prompt plus an extra run-specific instruction:

```bash
qwen -p "Summarize this repository" \
  --system-prompt "You are a migration planner." \
  --append-system-prompt "Return exactly three bullets."
```

### Choose an Output Style

Use `--output-style` to pick an [output style](./output-styles) — built-in or custom — for this run. A style is a named block of instructions layered onto the built-in prompt that changes how the answer is written — `Concise` leads with the result and drops preamble and narration, `Proactive` starts working instead of proposing, `Explanatory` adds short notes about the codebase along the way. It overrides the `general.outputStyle` setting; `default` selects no style.

```bash
qwen -p "Why does the build fail on Windows?" --output-style Concise
```

> [!note]
>
> - `Learning` asks you to write part of the code and waits for a reply, so it is skipped in headless runs.
> - An unknown style name prints a warning and the run continues with the default style.
> - `--output-style` has no effect when `--system-prompt` or `QWEN_SYSTEM_MD` replaces the built-in prompt — a style is only layered onto the built-in prompt.

> [!note]
>
> - `--system-prompt` applies only to the current run's main session.
> - Loaded memory and context files such as `QWEN.md` are still appended after `--system-prompt`.
> - `--append-system-prompt` is applied after the built-in prompt and loaded memory, and can be used together with `--system-prompt`.

## Output Formats

Qwen Code supports multiple output formats for different use cases:

### Text Output (Default)

Standard human-readable output:

```bash
qwen -p "What is the capital of France?"
```

Response format:

```
The capital of France is Paris.
```

### JSON Output

Returns structured data as a JSON array. All messages are buffered and output together when the session completes. This format is ideal for programmatic processing and automation scripts.

The JSON output is an array of message objects. The output includes multiple message types: system messages (session initialization), assistant messages (AI responses), and result messages (execution summary).

#### Example Usage

```bash
qwen -p "What is the capital of France?" --output-format json
```

Output (at end of execution):

```json
[
  {
    "type": "system",
    "subtype": "session_start",
    "uuid": "...",
    "session_id": "...",
    "model": "qwen3-coder-plus",
    ...
  },
  {
    "type": "assistant",
    "uuid": "...",
    "session_id": "...",
    "message": {
      "id": "...",
      "type": "message",
      "role": "assistant",
      "model": "qwen3-coder-plus",
      "content": [
        {
          "type": "text",
          "text": "The capital of France is Paris."
        }
      ],
      "usage": {...}
    },
    "parent_tool_use_id": null
  },
  {
    "type": "result",
    "subtype": "success",
    "uuid": "...",
    "session_id": "...",
    "is_error": false,
    "duration_ms": 1234,
    "result": "The capital of France is Paris.",
    "usage": {...}
  }
]
```

### Stream-JSON Output

Stream-JSON format emits JSON messages immediately as they occur during execution, enabling real-time monitoring. This format uses line-delimited JSON where each message is a complete JSON object on a single line.

```bash
qwen -p "Explain TypeScript" --output-format stream-json
```

Output (streaming as events occur):

```json
{"type":"system","subtype":"session_start","uuid":"...","session_id":"..."}
{"type":"assistant","uuid":"...","session_id":"...","message":{...}}
{"type":"result","subtype":"success","uuid":"...","session_id":"..."}
```

When combined with `--include-partial-messages`, additional stream events are emitted in real-time (message_start, content_block_delta, etc.) for real-time UI updates.

For JSON and stream-JSON output, textual `tool_result.content` values are
bounded to 65,536 UTF-8 bytes after JSON string serialization. Oversized
values are emitted as deterministic head/tail previews. The same bound applies
to persistent stream-JSON sessions, SDK transports, subagent tool results, and
Dual Output. Text mode still prints only the final response, while retaining
only the bounded preview internally. This limit does not cap an entire JSON
session, JSONL event, tool input, or partial message.

```bash
qwen -p "Write a Python script" --output-format stream-json --include-partial-messages
```

### Input Format

The `--input-format` parameter controls how Qwen Code consumes input from standard input:

- **`text`** (default): Standard text input from stdin or command-line arguments
- **`stream-json`**: JSON message protocol via stdin for bidirectional communication

> **Note:** Stream-json input mode is currently under construction and is intended for SDK integration. It requires `--output-format stream-json` to be set.

### File Redirection

Save output to files or pipe to other commands:

```bash
# Save to file
qwen -p "Explain Docker" > docker-explanation.txt
qwen -p "Explain Docker" --output-format json > docker-explanation.json

# Append to file
qwen -p "Add more details" >> docker-explanation.txt

# Pipe to other tools
qwen -p "What is Kubernetes?" --output-format json | jq -r '.[-1].result'
qwen -p "Explain microservices" | wc -w
qwen -p "List programming languages" | grep -i "python"

# Stream-JSON output for real-time processing
qwen -p "Explain Docker" --output-format stream-json | jq '.type'
qwen -p "Write code" --output-format stream-json --include-partial-messages | jq '.event.type'
```

## Configuration Options

Key command-line options for headless usage:

| Option                       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                             | Example                                                                  |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `--prompt`, `-p`             | Run in headless mode                                                                                                                                                                                                                                                                                                                                                                                                                                    | `qwen -p "query"`                                                        |
| `--output-format`, `-o`      | Specify output format (text, json, stream-json)                                                                                                                                                                                                                                                                                                                                                                                                         | `qwen -p "query" --output-format json`                                   |
| `--input-format`             | Specify input format (text, stream-json)                                                                                                                                                                                                                                                                                                                                                                                                                | `qwen --input-format text --output-format stream-json`                   |
| `--include-partial-messages` | Include partial messages in stream-json output                                                                                                                                                                                                                                                                                                                                                                                                          | `qwen -p "query" --output-format stream-json --include-partial-messages` |
| `--system-prompt`            | Override the main session system prompt for this run                                                                                                                                                                                                                                                                                                                                                                                                    | `qwen -p "query" --system-prompt "You are a terse reviewer."`            |
| `--append-system-prompt`     | Append extra instructions to the main session system prompt for this run                                                                                                                                                                                                                                                                                                                                                                                | `qwen -p "query" --append-system-prompt "Focus on concrete findings."`   |
| `--output-style`             | Output style for this run (`Concise`, `Proactive`, `Explanatory`, `Learning`, a custom style's name, or `default` for none); overrides `general.outputStyle`                                                                                                                                                                                                                                                                                            | `qwen -p "query" --output-style Concise`                                 |
| `--debug`, `-d`              | Enable debug mode                                                                                                                                                                                                                                                                                                                                                                                                                                       | `qwen -p "query" --debug`                                                |
| `--safe-mode`                | Disable all customizations — context files, hooks, extensions, skills, MCP servers, custom subagents (only built-in subagents load), permission rules, settings-sourced approval mode overrides, memory features, cross-session messaging, and sandbox settings — to isolate problems; the CLI flags `--yolo` and `--approval-mode` still take effect. See [Troubleshooting](../support/troubleshooting). Also settable via `QWEN_CODE_SAFE_MODE=true`. | `qwen -p "query" --safe-mode`                                            |
| `--model`, `-m`              | Model to use for this run                                                                                                                                                                                                                                                                                                                                                                                                                               | `qwen -p "query" --model qwen3-coder-plus`                               |
| `--include-directories`      | Include additional directories                                                                                                                                                                                                                                                                                                                                                                                                                          | `qwen -p "query" --include-directories src,docs`                         |
| `--yolo`, `-y`               | Auto-approve all actions                                                                                                                                                                                                                                                                                                                                                                                                                                | `qwen -p "query" --yolo`                                                 |
| `--approval-mode`            | Set approval mode (`plan`, `default`, `auto-edit`, `auto`, `yolo`)                                                                                                                                                                                                                                                                                                                                                                                      | `qwen -p "query" --approval-mode auto-edit`                              |
| `--continue`                 | Resume the most recent session for this project                                                                                                                                                                                                                                                                                                                                                                                                         | `qwen --continue -p "Pick up where we left off"`                         |
| `--resume [sessionId]`       | Resume a specific session (or choose interactively)                                                                                                                                                                                                                                                                                                                                                                                                     | `qwen --resume 123e... -p "Finish the refactor"`                         |
| `--max-session-turns`        | Cap the number of user/model/tool turns in the run                                                                                                                                                                                                                                                                                                                                                                                                      | `qwen -p "..." --max-session-turns 30`                                   |
| `--max-wall-time`            | Wall-clock budget; accepts `90` (s), `30s`, `5m`, `1h`, `1.5h`                                                                                                                                                                                                                                                                                                                                                                                          | `qwen -p "..." --max-wall-time 10m`                                      |
| `--max-tool-calls`           | Cumulative tool-call budget for the run                                                                                                                                                                                                                                                                                                                                                                                                                 | `qwen -p "..." --max-tool-calls 50`                                      |

For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](../configuration/settings).

### Tool schemas in small context windows

`--max-tool-calls 0` limits tool execution; it does not remove tool schemas from
the model request. `--safe-mode` also keeps built-in tools and ignores the
settings-sourced `tools.eager` allowlist, so it is not a text-only mode.

Outside safe or bare mode, use [`tools.eager`](../configuration/settings#tools)
to defer unlisted, non-exempt tool schemas. An empty list defers all non-exempt
eager tools, but exempt tools and other schema-reveal paths can still contribute
to the request. With safe mode enabled, use `--exclude-tools <name,...>` to remove
unneeded built-in tools instead. Exclusions disable those tools, rather than
keeping them available through deferred discovery. Recheck the tool names and
resulting request after upgrading; the built-in set can change between releases.

Budget the complete serialized input, including system instructions, history and
tool schemas, together with the requested output. Deferral and output limits do
not guarantee that the input fits: an oversized initial request can still be
rejected before generation. Do not rely on a schema-size preflight warning to
establish compatibility with a provider's context limit.

## Safety in unattended runs

Headless / CI runs combined with `--yolo` (or `--approval-mode=yolo`) auto-approve every tool call, including `shell`, `write`, and `edit`. **`--yolo` does not enable a sandbox** — those tools run at the host process's privilege level. When Qwen Code detects this combination with no sandbox configured, it prints a one-line warning to stderr at startup. Suppress the warning with `QWEN_CODE_SUPPRESS_YOLO_WARNING=1` once you've reviewed the trade-off.

### Run-level budgets

Qwen Code can abort an unattended run when it crosses one of the following thresholds. Each is `-1` (unlimited) by default; setting any one is enough to bound runaway behavior. They are enforced cooperatively against the same `AbortController` that already carries SIGINT, so a budget abort emits a structured `FatalBudgetExceededError` (exit code **55**) — distinct from the turn-cap exit code 53 and SIGINT's 130 so CI scripts can branch on the reason.

| Flag                  | Settings key               | What it bounds                                                                                                                                                                                                |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--max-wall-time`     | `model.maxWallTimeSeconds` | Wall-clock duration of the whole run. Flag accepts `90` (s), `30s`, `5m`, `1h`, `1.5h` (fractional units supported). Minimum 1s — sub-second values are rejected as typos. Settings is seconds.               |
| `--max-tool-calls`    | `model.maxToolCalls`       | Cumulative top-level tool calls dispatched by the main run loop (counts successes _and_ failures — the model still consumes tokens on errors). See "Scope" below for subagent / structured-output exemptions. |
| `--max-session-turns` | `model.maxSessionTurns`    | Number of user/model/tool turns; pre-existing. Exits with code 53 on overrun (distinct from budget exit 55).                                                                                                  |

#### Scope

- **`--max-tool-calls` counts top-level dispatches only.** When the model calls the `agent` tool, the dispatch counts as **1**; inner tool calls performed by the spawned subagent are **not** counted. A model that funnels work through subagents can do unbounded inner work under a small top-level budget. Combine with `--exclude-tools agent` if you need a tighter cap.
- **`structured_output` is exempt from `--max-tool-calls`.** Under `--json-schema`, the model's terminal `structured_output` call is the "I'm done" contract, not real work — it doesn't count against `--max-tool-calls` so a budget-edge completion isn't aborted as a false positive. The exemption is unconditional (including failed Ajv validations), so a model stuck in a malformed-output retry loop is NOT bounded by `--max-tool-calls`; combine with `--max-session-turns` or `--max-wall-time` to cap retries.
- **`structured_output` is NOT exempt from `--max-session-turns`.** That counter is pre-existing and bumps for every turn including the terminal contract. Size `--max-session-turns` to `N+1` if you want to allow `N` real-work turns under `--json-schema`.
- **Single-shot vs `--input-format stream-json`:** in stream-json input mode the daemon resets the budget counters at the start of every user message; the budget is per-message, not per-process.
- **`qwen serve` / ACP sessions:** the daemon ACP session path does NOT currently consult `--max-wall-time` / `--max-tool-calls` from settings.json. These budgets only apply to single-shot `qwen -p` runs and to `--input-format stream-json` sessions. (`qwen serve` does emit the YOLO-no-sandbox warning at boot if `tools.approvalMode: 'yolo'` is set in settings.)

### Recommended combinations

- **Trusted, isolated environment (ephemeral CI runner, container):** `qwen -p "..." --yolo --max-session-turns N --max-wall-time 10m --output-format json`. Pin a turn budget and a wall-clock budget so a stuck agent can't burn through your CI minutes, and capture `--output-format json` for post-run usage / tool-call auditing.
- **Local machine or shared infra:** also pass `--sandbox` (or set `QWEN_SANDBOX=1`) so shell / write / edit tools run inside the sandbox image.
- **Long-running CI with retry-on-rate-limit:** combine `QWEN_CODE_UNATTENDED_RETRY=1` with `--max-wall-time`. The retry env keeps the run alive past transient 429 / 529 responses; the wall-clock budget ensures a persistently-failing provider can't extend the job indefinitely.
- **Bounded auditing / exploration:** for read-only tasks, `--max-tool-calls 25` caps how aggressively the model can grep / read. Combine with `--exclude-tools shell,write,edit` to make the bound meaningful.

## Examples

### Code review

```bash
cat src/auth.py | qwen -p "Review this authentication code for security issues" > security-review.txt
```

### Generate commit messages

```bash
result=$(git diff --cached | qwen -p "Write a concise commit message for these changes" --output-format json)
echo "$result" | jq -r '.[-1].result'
```

### API documentation

```bash
result=$(cat api/routes.js | qwen -p "Generate OpenAPI spec for these routes" --output-format json)
echo "$result" | jq -r '.[-1].result' > openapi.json
```

### Batch code analysis

```bash
for file in src/*.py; do
    echo "Analyzing $file..."
    result=$(cat "$file" | qwen -p "Find potential bugs and suggest improvements" --output-format json)
    echo "$result" | jq -r '.[-1].result' > "reports/$(basename "$file").analysis"
    echo "Completed analysis for $(basename "$file")" >> reports/progress.log
done
```

### PR code review

```bash
result=$(git diff origin/main...HEAD | qwen -p "Review these changes for bugs, security issues, and code quality" --output-format json)
echo "$result" | jq -r '.[-1].result' > pr-review.json
```

### Log analysis

```bash
grep "ERROR" /var/log/app.log | tail -20 | qwen -p "Analyze these errors and suggest root cause and fixes" > error-analysis.txt
```

### Release notes generation

```bash
result=$(git log --oneline v1.0.0..HEAD | qwen -p "Generate release notes from these commits" --output-format json)
response=$(echo "$result" | jq -r '.[-1].result')
echo "$response"
echo "$response" >> CHANGELOG.md
```

### Model and tool usage tracking

```bash
result=$(qwen -p "Explain this database schema" --include-directories db --output-format json)
total_tokens=$(echo "$result" | jq -r '.[-1].stats.models // {} | to_entries | map(.value.tokens.total) | add // 0')
models_used=$(echo "$result" | jq -r '.[-1].stats.models // {} | keys | join(", ") | if . == "" then "none" else . end')
tool_calls=$(echo "$result" | jq -r '.[-1].stats.tools.totalCalls // 0')
tools_used=$(echo "$result" | jq -r '.[-1].stats.tools.byName // {} | keys | join(", ") | if . == "" then "none" else . end')
echo "$(date): $total_tokens tokens, $tool_calls tool calls ($tools_used) used with models: $models_used" >> usage.log
echo "$result" | jq -r '.[-1].result' > schema-docs.md
echo "Recent usage trends:"
tail -5 usage.log
```

## Persistent Retry Mode

When Qwen Code runs in CI/CD pipelines or as a background daemon, a brief API outage (rate limiting or overload) should not kill a multi-hour task. **Persistent retry mode** makes Qwen Code retry transient API errors indefinitely until the service recovers.

### How it works

- **Transient errors only**: HTTP 429 (Rate Limit) and 529 (Overloaded) are retried indefinitely. Other errors (400, 500, etc.) still fail normally.
- **Exponential backoff with cap**: Retry delays grow exponentially but are capped at **5 minutes** per retry.
- **Heartbeat keepalive**: During long waits, a status line is printed to stderr every **30 seconds** to prevent CI runners from killing the process due to inactivity.
- **Graceful degradation**: Non-transient errors and interactive mode are completely unaffected.

### Activation

Set the `QWEN_CODE_UNATTENDED_RETRY` environment variable to `true` or `1` (strict match, case-sensitive):

```bash
export QWEN_CODE_UNATTENDED_RETRY=1
```

> [!important]
> Persistent retry requires an **explicit opt-in**. `CI=true` alone does **not** activate it — silently turning a fast-fail CI job into an infinite-wait job would be dangerous. Always set `QWEN_CODE_UNATTENDED_RETRY` explicitly in your pipeline configuration.

### Examples

#### GitHub Actions

```yaml
- name: Automated code review
  env:
    QWEN_CODE_UNATTENDED_RETRY: '1'
  run: |
    qwen -p "Review all files in src/ for security issues" \
      --output-format json \
      --yolo > review.json
```

#### Overnight batch processing

```bash
export QWEN_CODE_UNATTENDED_RETRY=1
qwen -p "Migrate all callback-style functions to async/await in src/" --yolo
```

#### Background daemon

```bash
QWEN_CODE_UNATTENDED_RETRY=1 nohup qwen -p "Audit all dependencies for known CVEs" \
  --output-format json > audit.json 2> audit.log &
```

### Monitoring

During persistent retry, heartbeat messages are printed to **stderr**:

```
[qwen-code] Waiting for API capacity... attempt 3, retry in 45s
[qwen-code] Waiting for API capacity... attempt 3, retry in 15s
```

These messages keep CI runners alive and let you monitor progress. They do not appear in stdout, so JSON output piped to other tools remains clean.

## Resources

- [CLI Configuration](../configuration/settings#command-line-arguments) - Complete configuration guide
- [Authentication](../configuration/auth.md) - Setup authentication
- [Commands](../features/commands) - Interactive commands reference
- [Tutorials](../quickstart) - Step-by-step automation guides

---

### Users Features Hooks (`docs/users/features/hooks.md`)

# Qwen Code Hooks

## Overview

Qwen Code hooks provide a powerful mechanism for extending and customizing the behavior of the Qwen Code application. Hooks allow users to execute custom scripts or programs at specific points in the application lifecycle, such as before tool execution, after tool execution, at session start/end, and during other key events.

Hooks are enabled by default. You can temporarily disable all hooks by setting `disableAllHooks` to `true` in your settings file (at the top level, alongside `hooks`):

```json
{
  "disableAllHooks": true,
  "hooks": {
    "PreToolUse": [...]
  }
}
```

This disables all hooks without deleting their configurations.

## What are Hooks?

Hooks are user-defined scripts or programs that are automatically executed by Qwen Code at predefined points in the application flow. They allow users to:

- Monitor and audit tool usage
- Enforce security policies
- Inject additional context into conversations
- Customize application behavior based on events
- Integrate with external systems and services
- Modify tool inputs or responses programmatically

### Browsing your hooks

Run `/hooks` to open a read-only browser of the hooks this session runs. It moves from events to matchers to the individual hooks under a matcher; events without matcher support go straight to their hooks. A hook's details show its type, where it comes from, whether it is enabled, its command, URL or prompt, and, when they are set, its timeout, status message, HTTP `if` condition and whether it runs once or in the background. Hooks registered for the current session by skills or the SDK are listed with the source Session.

The browser never edits your configuration: to add, change or remove a hook, edit `settings.json`. It does re-read it. Opening the interactive menu reloads hook definitions from the user, workspace and system (System and SystemDefaults) settings files used by this session, including when the session runs in a worktree, so definitions you added, changed or removed since the session started take effect without a restart. If the user or workspace file cannot be read or parsed, both previous settings snapshots and the running hooks are retained, the files are left untouched, and an error is shown. If a system settings file cannot be read or parsed, the hooks from the system files stay as they were, your other edits still take effect, the file is left untouched, and an error names it.

Reloading requires this explicit menu-open action: saving a file, pulling changes or switching branches does not automatically arm new hook commands. The non-interactive `/hooks list` only displays the registry currently loaded by that process; it does not reload settings. In an interactive terminal, `/hooks list` opens the same menu as `/hooks`.

This reload covers hook definitions, not hook controls or HTTP security settings. Changes to `disableAllHooks`, `stopHookBlockingCap`, `security.allowedHttpHookUrls` and `security.allowPrivateNetworkHooks` still require a restart. Hooks registered at runtime by skills or the SDK are not affected. Project hooks load only in a trusted folder. When hooks are turned off by `disableAllHooks`, `--safe-mode` or `--bare`, none load and the browser says so at the top.

### Agent frontmatter scope

Hooks declared in an agent's frontmatter apply only to that invocation, including
its `SubagentStart` and `SubagentStop` events. They do not inherit into nested
agents or run for parents and siblings. Global settings hooks and session-wide
skill/function hooks retain their existing scope. Project-agent hooks recheck
the trust of the workspace that supplied the agent before every event.

Use `SubagentStop` for agent completion; `Stop` is not automatically remapped.
Move an agent hook to settings if it should observe the whole session.

## Hook Types

Qwen Code supports four hook executor types:

| Type       | Description                                                                                    |
| :--------- | :--------------------------------------------------------------------------------------------- |
| `command`  | Execute a shell command. Receives JSON via `stdin`, returns results via `stdout`.              |
| `http`     | Send JSON as a `POST` request body to a specified URL. Returns results via HTTP response body. |
| `function` | Directly call a registered JavaScript function (session-level hooks only).                     |
| `prompt`   | Use an LLM to evaluate hook input and return a decision.                                       |

### Command Hooks

Command hooks execute commands via child processes. Input JSON is passed through stdin, and output is returned via stdout.

**Configuration:**

| Field           | Type                     | Required | Description                                 |
| :-------------- | :----------------------- | :------- | :------------------------------------------ |
| `type`          | `"command"`              | Yes      | Hook type                                   |
| `command`       | `string`                 | Yes      | Command to execute                          |
| `name`          | `string`                 | No       | Hook name (for logging)                     |
| `description`   | `string`                 | No       | Hook description                            |
| `timeout`       | `number`                 | No       | Timeout in seconds, default 60              |
| `async`         | `boolean`                | No       | Whether to run asynchronously in background |
| `env`           | `Record<string, string>` | No       | Environment variables                       |
| `shell`         | `"bash" \| "powershell"` | No       | Shell to use                                |
| `statusMessage` | `string`                 | No       | Status message displayed during execution   |

`timeout` is in seconds for command, HTTP and prompt hooks; SDK-registered function hooks keep milliseconds. Command hook timeouts used to be written in milliseconds, so for command hooks a value of `1000` or more is still read as milliseconds and existing settings keep working. To migrate, look for command hooks whose `timeout` is `1000` or more and rewrite the value in seconds, for example `10000` as `10`. To give a command hook a timeout of 1000 seconds or more, keep writing it in milliseconds, for example `1800000` for 30 minutes. A command hook `timeout` that is not a positive number, such as `"30s"`, is ignored and the 60 second default applies. With debug logging enabled (`QWEN_DEBUG_LOG_FILE=1`), each command hook with a millisecond or ignored `timeout` is named once per session in that session's debug log.

**Example:**

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "write_file",
        "hooks": [
          {
            "type": "command",
            "command": "\"$QWEN_PROJECT_DIR/.qwen/hooks/security-check.sh\"",
            "name": "security-check",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

`QWEN_PROJECT_DIR`, `CLAUDE_PROJECT_DIR` and `GEMINI_PROJECT_DIR` are set to the project directory in the environment of every command hook. Bash hooks read them from the environment, so double-quote them like any other shell variable, as in `"$QWEN_PROJECT_DIR/.qwen/hooks/security-check.sh"`. For cmd hooks, and for a bare `$QWEN_PROJECT_DIR` in PowerShell hooks, the variable is replaced with the quoted project directory before the command runs; `$env:QWEN_PROJECT_DIR` also works in PowerShell.

Migration: bash hooks used to have these variables replaced in the command text before the shell ran, and that replacement has been removed. A bash hook that leaves the variable unquoted in a project path containing spaces, or writes it inside single quotes such as `'$QWEN_PROJECT_DIR/hook.sh'`, must now double-quote it: `"$QWEN_PROJECT_DIR/hook.sh"`.

### HTTP Hooks

HTTP hooks send hook input as POST requests to specified URLs. They support URL whitelists, DNS-level SSRF protection, environment variable interpolation, and other security features.

**Configuration:**

| Field            | Type                     | Required | Description                                               |
| :--------------- | :----------------------- | :------- | :-------------------------------------------------------- |
| `type`           | `"http"`                 | Yes      | Hook type                                                 |
| `url`            | `string`                 | Yes      | Target URL                                                |
| `headers`        | `Record<string, string>` | No       | Request headers (supports env var interpolation)          |
| `allowedEnvVars` | `string[]`               | No       | Whitelist of environment variables allowed in URL/headers |
| `timeout`        | `number`                 | No       | Timeout in seconds, default 600                           |
| `name`           | `string`                 | No       | Hook name (for logging)                                   |
| `statusMessage`  | `string`                 | No       | Status message displayed during execution                 |
| `once`           | `boolean`                | No       | Execute only once per event per session (HTTP hooks only) |

**Security Features:**

- **URL Whitelist**: Configure allowed URL patterns via `allowedUrls`
- **SSRF Protection**: Blocks private IPs (10.x.x.x, 172.16-31.x.x, 192.168.x.x, etc.) but allows loopback addresses (127.0.0.1, ::1)
- **DNS Validation**: Validates domain resolution before requests to prevent DNS rebinding attacks
- **Environment Variable Interpolation**: `${VAR}` syntax, only allows variables in `allowedEnvVars` whitelist

#### Allowing private-network hooks (managed environments only)

By default, HTTP hooks cannot target private or link-local IP ranges. In platform-managed environments where the hook receiver is a first-party, VPC-internal endpoint (for example, an internal API gateway resolving to `172.16.0.0/12`), you can relax the IP-range checks with:

```json
{
  "security": {
    "allowPrivateNetworkHooks": true
  }
}
```

- This setting is **only honored from User, System, and SystemDefaults settings scopes**. A value set in Workspace (project) settings is ignored and logged as a warning, so a cloned repository can never self-grant this bypass.
- The flag relaxes only the general private/CGNAT/link-local **range** checks. Cloud metadata endpoints stay blocked in every configuration: the `BLOCKED_HOSTS` list is matched literally (`metadata.google.internal`, `metadata.azure.internal`, ...), and the metadata IPs `169.254.169.254` and `100.100.100.200` are blocked in all serialized forms (including IPv4-mapped IPv6 such as `::ffff:a9fe:a9fe`) and after DNS resolution.
- The `security.allowedHttpHookUrls` whitelist still applies independently. In managed environments, pair this flag with a whitelist so only the intended internal endpoints are reachable. A whitelist in Workspace (project) settings is honored only when no User, System, or SystemDefaults scope sets one; otherwise it is ignored and logged as a warning, so a repository can narrow where its hooks send data but never replace a whitelist you configured (an empty whitelist means "allow all").
- HTTP hooks never follow redirects. A 3xx response is treated like any other non-2xx status: a non-blocking hook failure, and the redirect target is never contacted.

> **Warning:** Enabling this flag lets hooks reach internal infrastructure on your network. Enable it only in trusted, managed settings — never in a repository you do not control.

**Example:**

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "http",
            "url": "http://127.0.0.1:8080/hooks/pre-tool-use",
            "headers": {
              "Authorization": "Bearer ${HOOK_API_KEY}"
            },
            "allowedEnvVars": ["HOOK_API_KEY"],
            "timeout": 10,
            "name": "remote-security-check"
          }
        ]
      }
    ]
  }
}
```

**Example: External Judgment Service Adapter**

The `remote-security-check` config above expects `http://127.0.0.1:8080/hooks/pre-tool-use` to
already be running a service that speaks this contract (POST `{tool_name, tool_input, ...}` in,
`hookSpecificOutput.permissionDecision` out). Here is a minimal, stdlib-only adapter that fills
in that missing half, wired to one concrete judgment backend so the whole thing is runnable and
testable end to end rather than a stub. Only the `review()` function is backend-specific — swap
its body and request/response shape for whichever service you use; everything else (the server,
the fail-open handling, the hook response shape) stays the same regardless of backend.

_Disclosure: the backend used below, [invinoveritas](https://api.babyblueviper.com), is a
service the author is affiliated with — used here because it was the one that could be
verified end to end for this example, not an endorsement. Any HTTP service that returns a
JSON verdict works equally well; only `review()` needs to change._

_Data handling: with `matcher: "*"`, the full `tool_input` of **every** tool call is sent to
the judgment backend — treat that input as sensitive (it may contain file contents, paths, or
secrets). Narrow the matcher (e.g. to `run_shell_command`) if you only need to judge shell
commands._

```
/* Detailed source-code truncated for AI context efficiency. */
```

Tested live end to end against the real production API above: a genuinely destructive input
(`{"tool_name": "run_shell_command", "tool_input": {"command": "rm -rf /important_data"}}`)
returned `permissionDecision: "deny"` with a real explanation; a benign one (`ls -la`) returned
`"allow"`. Fails open on any network/timeout/malformed-response issue from the judgment
backend, so an outage never blocks legitimate tool calls — same discipline the `command`-hook
examples above apply with their own exit codes.

### Function Hooks

Function hooks directly call registered JavaScript/TypeScript functions. They are used internally by the Skill system and are not currently exposed as a public API for end users.

**Note**: For most use cases, use **command hooks** or **HTTP hooks** instead, which can be configured in settings files.

### Prompt Hooks

Prompt hooks use an LLM to evaluate hook input and return a decision. This is useful for making intelligent decisions based on context, such as determining whether to allow or block an operation.

> **Data handling:** A prompt hook sends its event input to the configured model provider. When file-backed debug logging is enabled, the fully expanded prompt-hook request is also written to the session debug log. Treat hook input and debug logs as potentially sensitive.

**How it works:**

1. The hook input JSON is injected into your prompt using the `$ARGUMENTS` placeholder
2. The prompt is sent to an LLM (default: your current model)
3. The LLM returns a JSON response with the decision
4. Qwen Code processes the decision and continues or blocks execution accordingly

**Configuration:**

| Field           | Type       | Required | Description                                         |
| :-------------- | :--------- | :------- | :-------------------------------------------------- |
| `type`          | `"prompt"` | Yes      | Hook type                                           |
| `prompt`        | `string`   | Yes      | Prompt sent to LLM. Use `$ARGUMENTS` for hook input |
| `model`         | `string`   | No       | Model to use (defaults to your current model)       |
| `timeout`       | `number`   | No       | Timeout in seconds, default 30                      |
| `name`          | `string`   | No       | Hook name (for logging)                             |
| `description`   | `string`   | No       | Hook description                                    |
| `statusMessage` | `string`   | No       | Status message displayed during execution           |

**Response Format:**

The LLM must return JSON with the following structure:

```json
{
  "ok": true,
  "reason": "Explanation of the decision",
  "additionalContext": "Optional context to inject into the conversation"
}
```

| Field               | Description                                                                |
| :------------------ | :------------------------------------------------------------------------- |
| `ok`                | `true` to allow/continue, `false` to block/stop                            |
| `reason`            | Required when `ok` is `false`. Shown to the model to explain the block     |
| `additionalContext` | Optional. Additional context to inject into the conversation when allowing |

**Supported Events:**

Prompt hooks can be used with most hook events, including:

- `PreToolUse` - Evaluate whether to allow a tool call
- `PostToolUse` - Evaluate tool results and potentially inject context
- `Stop` - Determine whether to continue or stop
- `SubagentStop` - Evaluate subagent results
- `UserPromptSubmit` - Evaluate or enrich eligible model-bound prompts

**Example: Stop Hook**

```json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "You are evaluating whether Qwen Code should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed\n\nRespond with JSON: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"your explanation\"} to continue working.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
```

When `ok` is `false`, Qwen Code will continue working and use the `reason` as context for the next response.

**Example: PreToolUse Hook**

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "run_shell_command",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evaluate this tool call for security concerns. Tool input: $ARGUMENTS\n\nCheck for:\n- Dangerous commands (rm -rf, curl | sh, etc.)\n- Unauthorized access attempts\n- Data exfiltration patterns\n\nRespond with {\"ok\": true} if safe, or {\"ok\": false, \"reason\": \"concern\"} if blocked.",
            "model": "sonnet",
            "timeout": 30,
            "name": "security-evaluator"
          }
        ]
      }
    ]
  }
}
```

## Hook Events

Hooks fire at specific points during a Qwen Code session. Different events support different matchers to filter trigger conditions.

| Event                 | Triggered When                                                         | Matcher Target                                                   |
| :-------------------- | :--------------------------------------------------------------------- | :--------------------------------------------------------------- |
| `PreToolUse`          | Before tool execution                                                  | Tool id (`write_file`, `read_file`, `run_shell_command`, etc.)   |
| `PostToolUse`         | After successful tool execution                                        | Tool id                                                          |
| `PostToolUseFailure`  | After tool execution fails                                             | Tool id                                                          |
| `PostToolBatch`       | Once after every tool call in a batch has resolved                     | None (always fires)                                              |
| `UserPromptSubmit`    | Before supported model invocations                                     | None                                                             |
| `UserPromptExpansion` | After a slash command expands into a prompt, before the prompt is sent | Command name, without the leading `/`                            |
| `SessionStart`        | When session starts or resumes                                         | Source (`startup`, `resume`, `clear`, `compact`)                 |
| `SessionEnd`          | When session ends                                                      | Reason (`clear`, `logout`, `prompt_input_exit`, etc.)            |
| `SessionDelete`       | After an explicitly selected session is deleted                        | None                                                             |
| `MessageDisplay`      | Repeatedly, as the reply streams                                       | None (always fires)                                              |
| `Stop`                | Before the turn ends                                                   | None (always fires)                                              |
| `StopFailure`         | When an API error or loop detection ends the turn, instead of `Stop`   | Error type (`rate_limit`, `server_error`, `loop_detected`, etc.) |
| `SubagentStart`       | When subagent starts                                                   | Agent type (`Bash`, `Explorer`, `Plan`, etc.)                    |
| `SubagentStop`        | When subagent stops                                                    | Agent type                                                       |
| `PreCompact`          | Before conversation compaction                                         | Trigger (`manual`, `auto`)                                       |
| `PostCompact`         | After conversation compaction succeeds                                 | Trigger (`manual`, `auto`)                                       |
| `Notification`        | When notifications are sent                                            | Type (`permission_prompt`, `idle_prompt`, `auth_success`)        |
| `PermissionRequest`   | When permission dialog is shown                                        | Tool id                                                          |
| `PermissionDenied`    | When AUTO-mode classification denies a tool call                       | Tool id                                                          |
| `TodoCreated`         | When a new todo item is created                                        | None (always fires)                                              |
| `TodoCompleted`       | When a todo item is marked as completed                                | None (always fires)                                              |
| `InstructionsLoaded`  | When a context file such as `QWEN.md`, or a file it imports, is loaded | File path of the loaded file                                     |

### Matcher Patterns

`matcher` is a regular expression used to filter trigger conditions.

| Event Type          | Events                                                                                     | Matcher Support | Matcher Target                                                                                                                                         |
| :------------------ | :----------------------------------------------------------------------------------------- | :-------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tool Events         | `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied` | ✅ Regex        | Tool id: `write_file`, `read_file`, `run_shell_command`, etc.                                                                                          |
| Tool Events         | `PostToolBatch`                                                                            | ❌ No           | N/A                                                                                                                                                    |
| Subagent Events     | `SubagentStart`, `SubagentStop`                                                            | ✅ Regex        | Agent type: `Bash`, `Explorer`, etc.                                                                                                                   |
| Session Events      | `SessionStart`                                                                             | ✅ Regex        | Source: `startup`, `resume`, `clear`, `compact`                                                                                                        |
| Session Events      | `SessionEnd`                                                                               | ✅ Regex        | Reason: `clear`, `logout`, `prompt_input_exit`, etc.                                                                                                   |
| Session Events      | `SessionDelete`                                                                            | ❌ No           | N/A                                                                                                                                                    |
| Notification Events | `Notification`                                                                             | ✅ Regex        | Type: `permission_prompt`, `idle_prompt`, `auth_success`                                                                                               |
| Compact Events      | `PreCompact`, `PostCompact`                                                                | ✅ Regex        | Trigger: `manual`, `auto`                                                                                                                              |
| Todo Events         | `TodoCreated`, `TodoCompleted`                                                             | ❌ No           | N/A                                                                                                                                                    |
| Prompt Events       | `UserPromptSubmit`                                                                         | ❌ No           | N/A                                                                                                                                                    |
| Prompt Events       | `UserPromptExpansion`                                                                      | ✅ Regex        | Command name without the leading `/`, for example `init`                                                                                               |
| Stop Events         | `Stop`                                                                                     | ❌ No           | N/A                                                                                                                                                    |
| Stop Events         | `StopFailure`                                                                              | ✅ Regex        | Error type: `rate_limit`, `authentication_failed`, `billing_error`, `invalid_request`, `server_error`, `max_output_tokens`, `loop_detected`, `unknown` |
| Message Display     | `MessageDisplay`                                                                           | ❌ No           | N/A                                                                                                                                                    |
| Instruction Events  | `InstructionsLoaded`                                                                       | ✅ Regex        | File path of the loaded file                                                                                                                           |

**Matcher Syntax:**

- Empty string `""`, `"*"` or `".*"` matches all events of that type
- A matcher is first compared exactly. In a `|`-separated list such as `permission_prompt | idle_prompt`, the matcher matches when any entry, ignoring spaces around it, is `*`, `.*`, or exactly the value, unless the whole matcher starts with `^` or `(`, in which case it is only a regular expression. Only a `|` outside `[...]` and groups, and not escaped with a backslash, separates entries: the pipes in `notes\|todo\.md`, `foo[ |]bar` and `a(b | c)` are part of the regular expression, and the spaces around them are kept. List entries are never read as regular expressions on their own
- Otherwise the matcher is an unanchored regular expression (e.g., `^run_shell_command$`, `read_.*`, `(write_file|edit)`). For a list that does not start with `^` or `(`, the expression is built from the trimmed, non-empty entries (an escaped space at an entry's edge, as in `\.env\ |\.pem`, is kept), so a stray `|` as in `read_(file|edit)|` is ignored and never makes the matcher match everything, and a matcher of only `|` matches nothing. A matcher that starts with `^` or `(` is compiled exactly as written, so a trailing `|` there, as in `^write_file|`, does make it match everything. Because the expression is unanchored, `read` also matches `read_file` and `edit` also matches `notebook_edit`. Add `^` and `$` to match a whole value, and anchor exclusions as well: `^(?!write_file).*$` excludes `write_file`, while unanchored `(?!write_file).*` still matches it
- The same rules apply to every event that supports a matcher, and to hooks registered by skills
- Tool hooks receive the runtime tool id in `tool_name` (for example, `write_file`). Built-in display names such as `WriteFile` and `ReadFile` are also accepted as matcher aliases for compatibility, and so are the tool names permission rules accept, including Claude Code's `Bash`, `Read` and `Write`, so a matcher copied from a Claude Code config such as `"Bash"` or `"Write|Edit"` works unchanged. New configs should still prefer runtime ids. Aliases are only compared exactly, so anchor runtime ids (`^write_file$`), not display names. Each alias names exactly one tool: unlike a permission rule, a `Read` matcher does not also cover `grep_search` or `glob`, and a `Bash` matcher does not cover `monitor`.

**Examples:**

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^run_shell_command$",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'bash check' >> /tmp/hooks.log"
          }
        ]
      },
      {
        "matcher": "write_.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'write check' >> /tmp/hooks.log"
          }
        ]
      },
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "echo 'all tools' >> /tmp/hooks.log" }
        ]
      }
    ],
    "SubagentStart": [
      {
        "matcher": "^(Bash|Explorer)$",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'subagent check' >> /tmp/hooks.log"
          }
        ]
      }
    ]
  }
}
```

## Input/Output Rules

### Hook Input Structure

All hook executors receive the standardized event input. The delivery boundary depends on the executor:

| Hook type  | Input recipient                                                 |
| :--------- | :-------------------------------------------------------------- |
| `command`  | Child process through JSON on `stdin`                           |
| `http`     | Configured endpoint through a JSON `POST` body                  |
| `function` | Trusted in-process callback                                     |
| `prompt`   | Configured model provider after the input replaces `$ARGUMENTS` |

Function hooks are trusted code running in the Qwen process. They receive an in-process object, so fields must not be treated as immutable against a function hook.

Qwen does not control whether a hook process, endpoint, callback, or model provider retains or forwards its input. Review each configured executor's data-handling policy.

**Common Fields:**

```json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "hook_event_name": "string",
  "timestamp": "string",
  "permission_mode": "default | plan | auto_edit | auto | yolo",
  "agent_id": "string (only when the event fires inside a subagent)",
  "prompt_id": "string (when the event belongs to a model turn)",
  "source_type": "string (only when the session has a declared source)",
  "source_id": "string (only when the session source has an id)"
}
```

Event-specific fields are added based on the hook type. `permission_mode` is the session's approval mode unless the event reports the mode that applied to it, as tool and subagent events do. `agent_id` is present only when the event fires inside a subagent; `agent_type` is reported on `SessionStart`, `SubagentStart` and `SubagentStop`. `source_type` and `source_id` appear only in ACP sessions whose client declared a session source when creating the session.

Hook input is a forward-extensible JSON contract: new optional fields can be added to existing events. Consumers should ignore unknown fields. A strict decoder that rejects unknown properties must be updated to explicitly allow each new optional field before upgrading Qwen Code. For security-sensitive hooks, a decoder failure can change fail-open or fail-closed behavior, so administrators must validate the upgraded payload against the deployed hook before rollout.

### Hook Output Structure

Hook output is returned via `stdout` (command) or HTTP response body (http) as JSON.

**Exit Code Behavior (Command Hooks):**

| Exit Code | Behavior                                                                                                                                                                                                                                                                                                                                                                       |
| :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0`       | Success. A JSON object in `stdout` controls behavior. Any other `stdout`, including bare JSON values such as `42`, is plain text: it is added to the model context on `SessionStart`, `UserPromptSubmit` and `UserPromptExpansion`, and kept as a system message on other events. Output that looks like a JSON object but does not parse is never added to the model context. |
| `2`       | **Blocking error**. Ignores `stdout`, passes `stderr` as error feedback to the model.                                                                                                                                                                                                                                                                                          |
| Other     | Non-blocking error. `stderr` only shown in debug mode, execution continues.                                                                                                                                                                                                                                                                                                    |

Adding plain text to the model context applies only to a command hook's `stdout`. An HTTP hook's response body is read as JSON only when its `Content-Type` is `application/json`; any other non-empty body becomes a `systemMessage` on every event. An HTTP hook that adds context must return JSON with `hookSpecificOutput.additionalContext`.

**Output Structure:**

Hook output supports three categories of fields:

1. **Common Fields**: `continue`, `stopReason`, `suppressOutput`, `systemMessage`
2. **Top-level Decision**: `decision`, `reason` (used by some events)
3. **Event-specific Control**: `hookSpecificOutput` (must include `hookEventName`)

```json
{
  "continue": true,
  "decision": "allow",
  "reason": "Operation approved",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "additionalContext": "Additional context information"
  }
}
```

### Individual Hook Event Details

#### PreToolUse

**Purpose**: Executed before a tool is used to allow for permission checks, input validation, or context injection.

**Event-specific fields**:

```json
{
  "permission_mode": "default | plan | auto_edit | auto | yolo",
  "tool_name": "name of the tool being executed",
  "tool_input": "object containing the tool's input parameters",
  "tool_use_id": "unique identifier for this tool use instance (internal format, e.g., toolu_xxx)",
  "tool_call_id": "original API call ID from the LLM provider (e.g., call_xxx for OpenAI/Qwen) (optional)"
}
```

**Output Options**:

- `hookSpecificOutput.permissionDecision`: "allow", "deny", or "ask" (REQUIRED)
- `hookSpecificOutput.permissionDecisionReason`: explanation for the decision (REQUIRED)
- `hookSpecificOutput.updatedInput`: an object that replaces the tool's whole input (see [Replacing the tool input](#replacing-the-tool-input))
- `hookSpecificOutput.additionalContext`: text appended, after a blank line, to this call's tool result that the model sees next (for a call made from a Code Mode `exec` script, to the `exec` call's result; see below). It is delivered whatever the decision: after the tool's own output when it runs, or after the error message when it is denied, stopped, or fails. `<` and `>` are escaped.

The `permissionDecision` value controls whether the tool runs:

- `"allow"` — let the call continue; the usual permission checks and approval prompt still apply.
- `"deny"` — block the tool; it does not execute and an error is returned to the model.
- `"ask"` — pause and ask the user to confirm the tool call in the TUI before it runs. Confirming runs the tool once; declining cancels it. This confirmation takes the place of the usual approval prompt, and no permission rule, approval mode (including YOLO) or PermissionRequest hook approves the call instead; a PermissionRequest hook can still deny it, or replace its input, which the user then confirms. In contexts that cannot prompt for confirmation — headless (`--prompt`) runs and background subagents — `"ask"` falls back to `"deny"`.

For `"ask"`, the TUI displays `permissionDecisionReason` as literal text rather than interpreting inline Markdown. This keeps formatting markers and link targets visible to the user.

With `"ask"`, the hook runs once. Its `additionalContext` is delivered with the tool result if the user confirms, and dropped if the user declines or the call is cancelled. In ACP sessions (IDE integrations), `"ask"` is currently treated as `"deny"`, and the context is delivered with the denial.

`additionalContext` goes into the tool result. It does not change the tool input, the approval prompt, the error shown while the tool runs, or the user's prompt. Because it becomes part of the tool result, it is saved in the session transcript with it and can appear wherever the full tool output is shown, such as the Ctrl+O detail view or a resumed session; it is not marked apart from the tool's own output.

The text a call's PreToolUse hooks add is cut to `tools.truncateToolOutputThreshold` characters; in ACP sessions the same limit covers PreToolUse and PostToolUseFailure context together. The batch output limits still apply to the result as a whole.

The context is not delivered when the call is cancelled, or when the turn is cancelled before the tool results go back to the model, even if the tool itself completed.

In Code Mode (`tools.codeModeOnly`), a call made from an `exec` script returns its result to the script, not to the model. Its PreToolUse and PostToolUseFailure context is appended to the `exec` call's tool result instead, after the `exec` call's own context, and the value the script receives is unchanged.

##### Replacing the tool input

`updatedInput` replaces the tool's entire input. It is not merged with the original: a field you leave out is removed, and the tool's own default for it applies. An empty object `{}` is a replacement too.

```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "updatedInput": { "file_path": "/workspace/notes/today.md" }
  }
}
```

- A call's PreToolUse hooks run once, after the tool is known to be enabled and callable but before its input is validated or checked against permissions. A replacement can therefore correct an input that would have been rejected. When several calls from one turn are scheduled together, as in the interactive terminal UI, every call's hooks run before any of them runs.
- The replacement then goes through everything the original input would have: the tool's own validation, `permissions` rules, the approval mode, plan mode, and the approval prompt, which shows the replacement. `"allow"` does not let a replacement skip any of these.
- The hook cannot change which tool runs, its call id or its working directory. The model's own tool call stays as it was in the conversation; the approval prompt, the tool, its result and the PostToolUse event use the replacement.
- An `updatedInput` that is present but not a JSON object (`null`, an array, a string or a number) stops the call with an error; the original input is not used. When hooks run in parallel, they all receive the same input, the last replacement in configuration order wins, and an invalid replacement from any of them stops the call. Hooks that run sequentially each receive the previous replacement as `tool_input`.
- The older `hookSpecificOutput.tool_input` field only changes the input passed to the next sequential hook; it never changes the input the tool runs with. Use `updatedInput` instead. When an output has both, `updatedInput` wins.
- A shell command that a hook could still rewrite is not run in parallel with other tool calls.
- If a skill registers a PreToolUse hook while other calls from the same turn are still waiting to run, those calls are not run, because their hooks ran without it; the model is told to call the tool again.
- Changes the user makes in the approval prompt (for example, modifying the content in an editor) come after the hooks ran.
- `updatedInput` from an async hook is ignored.
- In ACP sessions, `"ask"` is still treated as `"deny"` even with a replacement.
- Tool calls run through a managed tool runtime, and fixed-policy calls made by an orchestrator, do not support `updatedInput` yet. A replacement stops the call with an error instead of running either input.

A PermissionRequest hook's `decision.updatedInput` is checked the same way: one that is not a JSON object stops the call, and a replacement is validated and checked against the permission rules before it runs, and the hook's `"allow"` counts only as the approval of that replacement. A replacement that needs a decision the hook cannot make, such as an answer from the user, goes to the approval prompt instead; in ACP sessions it is not run. In AUTO mode, a replacement is reviewed again before it runs.

**Note**: While standard hook output fields like `decision` and `reason` are technically supported by the underlying class, the official interface expects the `hookSpecificOutput` with `permissionDecision` and `permissionDecisionReason`.

**Example Output**:

```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Security policy blocks database writes",
    "additionalContext": "Current environment: production. Proceed with caution."
  }
}
```

#### PostToolUse

**Purpose**: Executed after a tool completes successfully to process results, log outcomes, or inject additional context.

**Event-specific fields**:

```json
{
  "permission_mode": "default | plan | auto_edit | auto | yolo",
  "tool_name": "name of the tool that was executed",
  "tool_input": "object containing the tool's input parameters",
  "tool_response": "object containing the tool's response",
  "tool_use_id": "unique identifier for this tool use instance (internal format, e.g., toolu_xxx)",
  "tool_call_id": "original API call ID from the LLM provider (e.g., call_xxx for OpenAI/Qwen) (optional)",
  "duration_ms": "tool execution time in milliseconds, excluding approval (optional)"
}
```

**Output Options**:

- `decision`: "allow", "deny", "block" (defaults to "allow" if not specified)
- `reason`: reason for the decision
- `hookSpecificOutput.additionalContext`: additional information to be included

**Example Output**:

```json
{
  "decision": "allow",
  "reason": "Tool executed successfully",
  "hookSpecificOutput": {
    "additionalContext": "File modification recorded in audit log"
  }
}
```

#### PostToolUseFailure

**Purpose**: Executed when a tool execution fails to handle errors, send alerts, or record failures.

**Event-specific fields**:

```json
{
  "permission_mode": "default | plan | auto_edit | auto | yolo",
  "tool_use_id": "unique identifier for the tool use (internal format, e.g., toolu_xxx)",
  "tool_call_id": "original API call ID from the LLM provider (e.g., call_xxx for OpenAI/Qwen) (optional)",
  "tool_name": "name of the tool that failed",
  "tool_input": "object containing the tool's input parameters",
  "error": "error message describing the failure",
  "is_interrupt": "boolean indicating if failure was due to user interruption (optional)",
  "duration_ms": "tool execution time in milliseconds when execution had started (optional)"
}
```

**Output Options**:

- `hookSpecificOutput.additionalContext`: error handling information, appended after the error in the tool result the model sees next. This applies both when the tool returns an error and when it throws. For a call made from a Code Mode `exec` script, it goes to the `exec` call's result instead (see [PreToolUse](#pretooluse)). `<` and `>` are escaped.
- Standard hook output fields

**Example Output**:

```json
{
  "hookSpecificOutput": {
    "additionalContext": "Error: File not found. Failure logged in monitoring system."
  }
}
```

#### PostToolBatch

**Purpose**: Runs once after all tool calls in a batch have resolved, before their results are returned to the model. Use it to review the batch as a whole, add context to it, or stop it.

**Matcher**: None. `PostToolBatch` always fires, and a `matcher` on it is ignored.

**Event-specific fields**:

```json
{
  "permission_mode": "default | plan | auto_edit | auto | yolo",
  "tool_calls": [
    {
      "tool_name": "runtime tool id, for example run_shell_command",
      "tool_input": "object containing the tool's input parameters",
      "tool_use_id": "the call id from the model's request",
      "tool_call_id": "the same call id (optional)",
      "status": "success | error | cancelled",
      "tool_response": "object with response_parts, result_display, error, error_type, execution_status, content_length and, when present, vision_bridge_notice (optional)"
    }
  ]
}
```

**Output Options**:

- `hookSpecificOutput.additionalContext`: appended to the result of the last call in the batch, after a blank line. `<` and `>` are escaped.
- `decision`: `"block"` or `"deny"` stops the batch, and so does `continue: false` or exit code 2. The last call's result is replaced by an error that carries `stopReason`, or `reason` when there is no `stopReason`. The other results are kept, and `additionalContext` is still appended.
- `stopReason` / `reason`: the text of that error.

If the hooks fail, or do not finish within 15 seconds, the batch continues with its results unchanged.

**Example Output**:

```json
{
  "decision": "block",
  "reason": "This batch edited files outside the allowed directory",
  "hookSpecificOutput": {
    "hookEventName": "PostToolBatch",
    "additionalContext": "Ask the user before retrying these edits."
  }
}
```

#### UserPromptSubmit

**Purpose**: Executed before supported model invocations to validate, block, or enrich their input. On the core/headless path, the event currently covers `UserQuery`, `ToolResult`, and `Hook` sends, while `Retry`, `Steer`, `Cron`, `Notification`, and `Teammate` sends are skipped. It can therefore occur on continuation paths, and `prompt` must not be assumed to be raw user input. The ACP session path has its own invocation policy: retries and newly dispatched background tasks can still invoke legacy hooks; continue, restored-question, and runtime-goal turns do not.

**Event-specific fields**:

```json
{
  "prompt": "legacy prompt for this invocation; semantics depend on the execution path",
  "submitted_prompt": "optional user text captured at a supported submission boundary"
}
```

`submitted_prompt` is optional. It is present on supported interactive TUI submissions and first-turn headless `UserQuery` sends. On the ACP session path used by ACP clients, `serve`, and daemon hosts, a fresh turn must carry an explicit submission declaration. Missing, non-string, empty, or whitespace-only declarations omit the field; the value is never reconstructed from `prompt` or a display label. Retries, continuations, and channel-classified turns omit it. The channel exclusion includes both automated events and human messages relayed through channel adapters.

Web Shell provides the original composer text at its submission boundary. Realtime voice handoffs do not declare provenance because their request text comes from model-generated tool arguments. Other ACP/daemon SDK clients can opt in per request with `_meta: { "qwen.submittedPrompt": "original submitted text" }`, captured before resource or model-only expansion. Existing clients without this declaration continue running legacy hooks but do not trigger provenance-gated Auto Recall. Do not add the declaration globally to an SDK transport: scheduled tasks, Live task runs, sub-session spawns, model-authored cross-session messages, and promoted mid-turn messages must not acquire it automatically. The private `qwen.daemon.submittedPrompt` key is reserved for the daemon-to-child hop and is stripped from external callers. These declarations are caller-supplied provenance, not proof of human authorship or authorization.

On the ACP path, the initial legacy `prompt` is the request's text blocks joined with a space before resource, attachment, slash-command, or model-only expansion. It does not expose the complete expanded model input. `submitted_prompt` can equal that text, but comes only from the explicit declaration and preserves its original whitespace. On the core/headless path, legacy `prompt` represents the current model-bound text for the hook invocation. Neither field is a complete DLP inspection surface.

In headless runs, an input that invokes a custom command or skill, including several stacked skills, is checked before the command expands. The hook runs once for that input, with `prompt` and `submitted_prompt` both set to the raw input, such as `/deploy staging`. A block stops the command before it runs: its `!{...}` shell commands do not run, a skill does not register its allowed tools or hooks, and no model request is made. When the input is allowed, its `additionalContext` is added once to the expanded prompt, and `UserPromptExpansion` still runs after the expansion. Hooks that the skill registers do not check the input that invoked it. Other slash commands, MCP prompts and saved workflows, and the interactive UI and ACP sessions, still run the hook on the expanded prompt.

The following composer rules apply to the interactive TUI, not to ACP clients. Deferred input can retain the field when its provenance remains complete. A combined batch retains provenance only when every constituent item has it; edited, partially known, or otherwise ambiguous input omits the field. Prompt, command, and shell-history navigation or selected search matches, cross-restart stash restores, and conversation rewind restores also omit it because those paths can surface model-bound text without its original provenance. Consumers that require user-submitted text should treat absence as unavailable rather than falling back to `prompt`.

After restored or provenance-unavailable model-bound input is cleared or submitted, the composer also clears its undo and redo history. This prevents undo from restoring expanded text after its marker or sidecar has been consumed.

Large-paste placeholders remain compact in `submitted_prompt`; the expanded pasted content appears only in `prompt`. On that TUI path, consumers should treat the field as a text projection rather than a byte-for-byte record of clipboard input. ACP clients have no equivalent built-in Vim, paste-placeholder, history, or rewind provenance tracking; they own whether restored or edited text retains a valid submission declaration.

Any non-empty input present while Vim mode is enabled omits `submitted_prompt`, including after Vim is disabled, because Vim registers do not carry provenance in this version. This conservative rule also covers drafts entered before enabling Vim. Clearing the composer starts a new eligible input.

This field is provenance, not authentication, tenant identity, authorization, or DLP. It is caller-supplied data. Every executor configured for this event receives it; in particular, HTTP hooks send it to their endpoint and prompt hooks send it to their model provider.

When both fields are present, prompt-hook payloads contain overlapping text and can consume additional model input tokens. There is no per-hook field suppression in this version.

Sequential UserPromptSubmit hooks can append `additionalContext` to `prompt`; `submitted_prompt` continues to represent the captured submission. Function hooks are trusted same-process code and are not constrained by an immutability guarantee.

When the final hook output contains non-empty `additionalContext`, Qwen first
sanitizes the value and then sends it to the model as a separate text part:

```xml
<qwen:user-prompt-submit-context>
sanitized hook context
</qwen:user-prompt-submit-context>
```

The tag tells the model and transcript consumers that the part came from a
configured hook rather than from the user prompt. It is a provenance marker,
not authentication, authorization, or a general trust boundary.

For a `UserQuery` with this added context, the session JSONL record preserves
the model-bound parts, including the tagged part, and adds the following
`systemPayload`:

```json
{
  "displayText": "pre-hook display projection",
  "hookContext": "sanitized hook context"
}
```

This two-field payload is written only for this kind of user-prompt record.
`hookContext` intentionally duplicates the tagged part so offline and
third-party consumers can identify its provenance without parsing model text.
`displayText` is the pre-hook display projection and never includes the hook
context. On the core/headless path it is the submitted projection when available, otherwise the expanded pre-hook prompt. ACP records the trusted display projection or raw request text before expansion when a projection, attachment references, or input annotations require a payload; otherwise it records the user message without `systemPayload` or `displayText`.

Transcript display consumers treat `displayText` as this user-prompt projection
when `systemPayload.hookContext` is a string. For compatibility with released
`displayText`-only user-prompt records, a complete tagged context in the final
part after at least one other part is equivalent pairing evidence. Notification,
cron, and mid-turn records can also have `displayText`, but those values are
compact display labels and must not be substituted for their model-bound text
without that evidence.
Legacy bare-context records keep their model-bound display behavior because the
context cannot be separated reliably. For metadata-free records that use the
current tagged shape, compatibility consumers may remove the same complete
final tagged part; they must not infer that arbitrary tag-like user text is hook
provenance.

Sensitive prompt telemetry attributes, when enabled, and managed auto-memory
recall both use the pre-hook prompt. They do not include
`UserPromptSubmit`-added context.

**Output Options**:

- `decision`: "allow", "deny", "block", or "ask"
- `reason`: human-readable explanation for the decision
- `hookSpecificOutput.additionalContext`: additional context to append to the prompt (optional)

When sent to the model, injected `additionalContext` is appended as its own message part wrapped in a reserved `<qwen:user-prompt-submit-context>...</qwen:user-prompt-submit-context>` tag, so it stays distinguishable from user-authored text in model history and session transcripts. Angle brackets in hook output are escaped before wrapping, so hook content cannot close or forge the tag. The session transcript also records the user's original prompt text separately; the interactive TUI and the ACP/export transcript-replay path display that original text rather than the injected context.

**Note**: Since UserPromptSubmitOutput extends HookOutput, all standard fields are available but only additionalContext in hookSpecificOutput is specifically defined for this event.

**Example Output**:

```json
{
  "decision": "allow",
  "reason": "Prompt reviewed and approved",
  "hookSpecificOutput": {
    "additionalContext": "Remember to follow company coding standards."
  }
}
```

#### UserPromptExpansion

**Purpose**: Runs when a slash command expands into a prompt for the model, after the expansion and before the prompt is sent. This covers commands that submit a prompt, such as custom commands, skills, MCP prompts and built-in commands like `/init`. Commands that only act locally do not fire it. It fires in the interactive UI, headless runs and ACP sessions, and when the model runs a model-invocable command.

**Matcher**: Matches against `command_name`. For example, `"matcher": "^init$"` fires only for `/init`.

**Event-specific fields**:

```json
{
  "command_name": "slash command name without the leading /",
  "command_args": "argument text after the command name",
  "prompt": "the expanded prompt, with non-text parts rendered as text"
}
```

When a headless or ACP message invokes several skills at once, `command_name` is their names joined by spaces and `command_args` is the remaining text.

**Output Options**:

- `decision`: `"block"` or `"deny"` stops the expanded prompt from being sent, and so does `continue: false` or exit code 2. The user sees `UserPromptExpansion blocked: <reason>`; for a model-invoked command, that message is returned as the error.
- `reason` / `stopReason`: the text shown in that message.
- `hookSpecificOutput.additionalContext`: appended to the expanded prompt after a blank line. `&`, `<` and `>` are escaped, and the result is cut to 10,000 characters.

A command hook that exits 0 with plain-text `stdout` has that text treated as `additionalContext` (see [Exit Code Behavior](#hook-output-structure)).

**Example Output**:

```json
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptExpansion",
    "additionalContext": "Follow the repository's CONTRIBUTING.md when generating QWEN.md."
  }
}
```

#### SessionStart

**Purpose**: Executed when a new session starts to perform initialization tasks.

**Event-specific fields**:

```json
{
  "permission_mode": "default | plan | auto_edit | auto | yolo",
  "source": "startup | resume | clear | compact",
  "model": "the model being used",
  "agent_type": "the type of agent if applicable (optional)"
}
```

**Output Options**:

- `hookSpecificOutput.additionalContext`: context to be available in the session
- Standard hook output fields

**Example Output**:

```json
{
  "hookSpecificOutput": {
    "additionalContext": "Session started with security policies enabled."
  }
}
```

#### SessionEnd

**Purpose**: Executed when a session ends to perform cleanup tasks.

**Event-specific fields**:

```json
{
  "reason": "clear | logout | prompt_input_exit | bypass_permissions_disabled | other"
}
```

**Output Options**:

- Standard hook output fields (typically not used for blocking)

#### SessionDelete

**Purpose**: Runs after an explicitly selected session has been permanently deleted. This event is fire-and-forget: output and failures cannot undo the deletion.

**Event-specific fields**:

```json
{
  "deleted_session_id": "the session that was deleted"
}
```

The hook uses the deleting runtime's normal session fields (`session_id`, `transcript_path`, and `cwd`); over ACP, `transcript_path` is empty because the deleting runtime has no transcript of its own. `SessionDelete` currently fires for the interactive `/delete` flow and ACP's explicit `deleteSession` method; daemon REST batch deletion and internal cleanup do not emit it. A command hook is left to finish if Qwen exits after dispatch; its stdout and stderr are ignored and remain independent of Qwen's pipes.

#### MessageDisplay

**Purpose**: Fires repeatedly as the assistant's reply streams — before `Stop`, which fires once at the end of the turn. Useful for live narration, incremental logging, or any consumer that wants to react to the reply as it's written rather than after the fact. This is a **fire-and-forget** event - hook output and exit codes are ignored.

**Event-specific fields**:

```json
{
  "message_id": "stable id for the whole streamed message",
  "displayed_text": "the CUMULATIVE text streamed so far for this message (not a delta)",
  "is_final": "true on the last firing for this message, false otherwise"
}
```

`displayed_text` is cumulative rather than a delta so hook scripts never need to reassemble chunks themselves — each firing carries the full text so far. Firing is debounced (at most every ~200ms) except for the final firing (`is_final: true`), which always fires once the message ends, so the reply's tail is never dropped waiting on the debounce window.

**Delivery semantics** — what a hook script can rely on:

- **Slow hooks see fewer, newer payloads.** At most one mid-stream hook execution per message is in flight at a time; while one runs, newer debounced payloads _replace_ the queued one rather than piling up behind it. A hook slower than the debounce window therefore skips intermediate snapshots — lossless, since each payload carries the full cumulative text.
- **`is_final` is never queued behind a stale delivery.** The final payload is dispatched the moment the message ends — alongside a still-running mid-stream execution if there is one (the one exception to the one-at-a-time rule, justified the same way: the final cumulative text strictly supersedes whatever that execution is processing). Your hook always receives the `is_final` payload, and receives it before the `Stop` hook fires. One consequence for stateful hooks: when the final execution overlaps a superseded mid-stream one, their _completion_ order is unspecified — the stale execution may finish after the final one (even after `Stop`). Treat `is_final` as terminal per `message_id` and let the cumulative text win, rather than assuming the last execution to finish carries the newest state.
- **The turn waits for `is_final` delivery to complete — but not forever.** The turn's end (and the `Stop` hook, when it fires) waits up to 5 seconds for the final delivery to finish. A hook that completes within that budget keeps the strongest guarantee: a headless run (`qwen -p ...`) exits only after the hook finished, and the `is_final` execution completes before `Stop` starts. A slower hook still receives `is_final` first — only the wait for its completion is bounded: in the terminal UI or an ACP session the execution simply finishes in the background, while a headless run exits without waiting. The hook process is not killed on exit; it is left to finish on its own, so a script chaining `qwen -p … && next-step` can observe `next-step` starting while a slow hook is still running. Hitting this timeout prints a warning on stderr.
- **Cancellation behaviour depends on timing.** A turn cancelled _before `is_final` dispatches_ fires no `is_final` — the message is treated as abandoned, and a consumer that buffers until `is_final` should treat cancellation-silence as its flush/discard signal (e.g. a timeout fallback). The criterion is the abort signal's state at the moment the turn ends, not whether every chunk had already streamed — an abort landing in the brief gap before that check can still suppress `is_final` for a message whose text had, in practice, finished arriving. Cancelling _after `is_final` has dispatched_ (during the drain wait) is different: the still-running hook execution may be terminated mid-flight (SIGTERM), but the payload itself has already been delivered.
- **`displayed_text` is provisional until `is_final`.** It reflects what has streamed so far; treat intermediate payloads as display state, not as authoritative final content.
- **A tool-using turn produces multiple messages.** Each model call gets its own `message_id` with its own `is_final: true` firing: the text before a tool call is one message, the continuation after the tool result is another. Model calls that produce no displayed text (tool-call-only) fire nothing.

**Note**: Fires in the terminal UI, headless (`-p`), and ACP (IDE/editor/`qwen serve`) sessions, with the same payload contract on every surface.

#### Stop

**Purpose**: Executed before Qwen concludes its response to provide final feedback or summaries.

**Event-specific fields**:

```json
{
  "stop_hook_active": "true when this turn is continuing because a stop hook blocked the previous stop check (still true after tool calls made during that continuation); false on the first check and again once the stop is allowed, the blocking cap is reached, the user steers or sends new input, or a new turn, retry or goal turn starts",
  "last_assistant_message": "the last message from the assistant",
  "context_usage": "ratio of context window used (may exceed 1 when tokens exceed window; optional)",
  "context_limit": "context window size in tokens (optional)",
  "input_tokens": "prompt token count (may include output tokens depending on provider; optional)",
  "background_tasks": "array of background tasks, each with id, status, agent_type, started_at and optional description",
  "crons": "array of scheduled jobs, each with id, schedule, prompt, recurring, enabled and optional next_run and last_run"
}
```

The `context_usage`, `context_limit`, and `input_tokens` fields allow hook scripts to observe context usage and implement custom compact strategies — for example, a script that prints a reminder to run `/compact` when usage exceeds a custom threshold.

**Output Options**:

- `decision`: "allow", "deny", "block", or "ask"
- `reason`: human-readable explanation for the decision
- `stopReason`: feedback to include in the stop response
- `continue`: set to false to stop execution
- `hookSpecificOutput.additionalContext`: additional context information

**Note**: Since StopOutput extends HookOutput, all standard fields are available but the stopReason field is particularly relevant for this event.

**Blocking cap**: consecutive blocking decisions are counted per prompt and keep counting across tool round trips, so a hook that blocks on every check ends the turn after `stopHookBlockingCap` blocks (default 8; `QWEN_CODE_STOP_HOOK_BLOCK_CAP` overrides it for one run). The count and `stop_hook_active` share the same record: both restart when the stop is allowed, when you steer or send new input, when a turn is retried, when a goal turn starts, and when the turn ends abnormally. In non-interactive text mode, Stop hook messages and cap warnings are written to stderr; JSON output is unchanged.

**Example Output**:

```json
{
  "decision": "block",
  "reason": "Must be provided when Qwen Code is blocked from stopping"
}
```

#### StopFailure

**Purpose**: Runs instead of `Stop` when an API error or loop detection ends the turn. API errors fire it in the interactive UI and in ACP sessions, but not in headless (`-p`) runs. Loop detection fires it in the interactive UI and in headless runs, but not in ACP sessions. This is a **fire-and-forget** event: Qwen does not wait for the hooks, and their output and exit codes are ignored.

**Event-specific fields**:

```json
{
  "error": "rate_limit | authentication_failed | billing_error | invalid_request | server_error | max_output_tokens | loop_detected | unknown",
  "error_details": "the error message, or the loop type for loop_detected (optional)",
  "last_assistant_message": "optional; the interactive UI currently sets it to the formatted error text"
}
```

For API errors, `error` is derived from the HTTP status and the error message: status 429 or "rate limit" gives `rate_limit`; 401 or "unauthorized" gives `authentication_failed`; 402, 403, "billing" or "quota" gives `billing_error`; 400 or "invalid" gives `invalid_request`; any other status of 500 or above gives `server_error`; "max_tokens" or "token limit" gives `max_output_tokens`; anything else is `unknown`. The checks run in that order.

**Matcher**: Matches against the `error` field. For example, `"matcher": "rate_limit"` will only trigger for rate limit errors.

**Output Options**:

- **None** - StopFailure is fire-and-forget. All hook output, errors and exit codes are discarded.

**Exit Code Handling**:

| Exit Code | Behavior                  |
| --------- | ------------------------- |
| Any       | Ignored (fire-and-forget) |

**Example Configuration**:

```json
{
  "hooks": {
    "StopFailure": [
      {
        "matcher": "rate_limit",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/rate-limit-alert.sh",
            "name": "rate-limit-alerter"
          }
        ]
      }
    ]
  }
}
```

**Use Cases**:

- Rate limit monitoring and alerting
- Authentication failure logging
- Billing error notifications
- Error statistics collection

A command hook is left to finish if Qwen exits after dispatch; its stdout and stderr are ignored and remain independent of Qwen's pipes.

#### SubagentStart

**Purpose**: Executed when a subagent (like the Task tool) is started to set up context or permissions.

**Event-specific fields**:

```json
{
  "permission_mode": "default | plan | auto_edit | auto | yolo",
  "agent_id": "identifier for the subagent",
  "agent_type": "type of agent (Bash, Explorer, Plan, Custom, etc.)"
}
```

**Output Options**:

- `hookSpecificOutput.additionalContext`: initial context for the subagent
- Standard hook output fields

**Example Output**:

```json
{
  "hookSpecificOutput": {
    "additionalContext": "Subagent initialized with restricted permissions."
  }
}
```

#### SubagentStop

**Purpose**: Executed when a subagent finishes to perform finalization tasks.

**Event-specific fields**:

```json
{
  "permission_mode": "default | plan | auto_edit | auto | yolo",
  "stop_hook_active": "false on the first stop check; true when the subagent is continuing because a SubagentStop hook blocked its previous stop",
  "agent_id": "identifier for the subagent",
  "agent_type": "type of agent",
  "agent_transcript_path": "path to the subagent's transcript",
  "last_assistant_message": "the last message from the subagent",
  "background_tasks": "array of background tasks, same shape as in Stop",
  "crons": "array of scheduled jobs, same shape as in Stop"
}
```

**Output Options**:

- `decision`: "allow", "deny", "block", or "ask"
- `reason`: human-readable explanation for the decision

**Example Output**:

```json
{
  "decision": "block",
  "reason": "Must be provided when Qwen Code is blocked from stopping"
}
```

#### PreCompact

**Purpose**: Executed before conversation compaction to prepare or log the compaction.

**Event-specific fields**:

```json
{
  "trigger": "manual | auto",
  "custom_instructions": "custom instructions currently set"
}
```

**Output Options**:

- `hookSpecificOutput.additionalContext`: context to include before compaction
- Standard hook output fields

**Example Output**:

```json
{
  "hookSpecificOutput": {
    "additionalContext": "Compacting conversation to maintain optimal context window."
  }
}
```

#### PostCompact

**Purpose**: Runs after a conversation compaction succeeds, to archive the summary or track usage. It does not fire when compaction fails. Qwen waits for the hooks to finish before continuing.

**Event-specific fields**:

```json
{
  "trigger": "manual | auto",
  "compact_summary": "the summary that replaced the compacted history, without its <analysis> block"
}
```

**Matcher**: Matches against the `trigger` field: `manual` when compaction was requested, for example with `/compress`, and `auto` when Qwen compacts on its own. For example, `"matcher": "manual"` fires only for requested compactions.

**Output Options**:

- **None** - PostCompact output is ignored. `decision`, `continue`, `additionalContext` and exit codes have no effect.

**Example Configuration**:

```json
{
  "hooks": {
    "PostCompact": [
      {
        "matcher": "manual",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/save-compact-summary.sh",
            "name": "save-summary"
          }
        ]
      }
    ]
  }
}
```

**Use Cases**:

- Summary archiving to files or databases
- Usage statistics tracking
- Context change monitoring
- Audit logging for compaction operations

#### Notification

**Purpose**: Executed when notifications are sent to customize or intercept them.

**Event-specific fields**:

```json
{
  "message": "notification message content",
  "title": "notification title (optional)",
  "notification_type": "permission_prompt | idle_prompt | auth_success"
}
```

> **Note**: `elicitation_dialog` type is defined but not currently implemented.

**Output Options**:

- `hookSpecificOutput.additionalContext`: additional information to include
- Standard hook output fields

**Example Output**:

```json
{
  "hookSpecificOutput": {
    "additionalContext": "Notification processed by monitoring system."
  }
}
```

#### PermissionRequest

**Purpose**: Executed when permission dialogs are displayed to automate decisions or update permissions.

**Event-specific fields**:

```json
{
  "permission_mode": "default | plan | auto_edit | auto | yolo",
  "tool_name": "name of the tool requesting permission",
  "tool_input": "object containing the tool's input parameters",
  "permission_suggestions": "array of suggested permissions (optional)"
}
```

**Output Options**:

- `hookSpecificOutput.decision`: structured object with permission decision details:
  - `behavior`: "allow" or "deny"
  - `updatedInput`: modified tool input (optional)
  - `updatedPermissions`: modified permissions (optional)
  - `message`: message to show to user (optional)
  - `interrupt`: whether to interrupt the workflow (optional)

**Example Output**:

```json
{
  "hookSpecificOutput": {
    "decision": {
      "behavior": "allow",
      "message": "Permission granted based on security policy",
      "interrupt": false
    }
  }
}
```

#### PermissionDenied

**Purpose**: Runs when the AUTO-mode classifier blocks a tool call, including when repeated blocks make Qwen fall back to asking the user. It reports the denial and cannot approve or change the call. Qwen waits for the hooks before continuing; a hook failure is logged and does not change the outcome.

**Matcher**: Matches against the tool id, like the other tool events.

**Event-specific fields**:

```json
{
  "tool_name": "runtime tool id of the denied call",
  "tool_input": "object containing the tool's input parameters",
  "tool_use_id": "the call id",
  "tool_call_id": "the same call id (optional)",
  "reason": "classifier_blocked | classifier_unavailable"
}
```

`reason` is currently always `classifier_blocked`. `classifier_unavailable` is defined, but a call denied because the classifier returned no verdict does not fire this event.

**Output Options**:

- **None** - PermissionDenied output and exit codes are ignored.

#### TodoCreated

**Purpose**: Executed when a new todo item is created via the `todo_write` tool. Allows validation, logging, or blocking of todo creation.

Todo hooks run in two phases:

- `validation`: runs before persistence. Use this phase for validation only; returning `block` or `deny` prevents the write.
- `postWrite`: runs after persistence. Use this phase for side effects such as logging or syncing; `block` or `deny` is ignored in this phase.

**Event-specific fields**:

```json
{
  "todo_id": "unique identifier for the todo item",
  "todo_content": "content/description of the todo item",
  "todo_status": "pending | in_progress | completed",
  "all_todos": "array of all todo items in the current list",
  "phase": "validation | postWrite"
}
```

**Output Options**:

- `decision`: "allow", "block", or "deny"
- `reason`: human-readable explanation for the decision (required when blocking)

**Blocking Behavior**:

During the `validation` phase, when `decision` is `block` or `deny` (exit code 2), todo creation is prevented. The todo list remains unchanged, and the reason is provided as feedback to the model.

During the `postWrite` phase, the todo has already been persisted. Hooks may still return output, but `block` / `deny` does not undo the write and should not be used for validation.

**Example Output (Allow)**:

```json
{
  "decision": "allow",
  "reason": "Todo content validated successfully"
}
```

**Example Output (Block)**:

```json
{
  "decision": "block",
  "reason": "Todo content too short. Minimum 5 characters required."
}
```

**Example Hook Script**:

```bash
#!/bin/bash
# ~/.qwen/hooks/todo-validator.sh
# Validates todo content before creation

INPUT=$(cat)
CONTENT=$(echo "$INPUT" | jq -r '.todo_content')

# Check minimum length
if [ ${#CONTENT} -lt 5 ]; then
  echo '{"decision": "block", "reason": "Todo content must be at least 5 characters"}'
  exit 2
fi

# Block test-related todos
if [[ "$CONTENT" =~ "test" ]]; then
  echo '{"decision": "block", "reason": "Test todos are not allowed in production"}'
  exit 2
fi

echo '{"decision": "allow"}'
exit 0
```

**Example Configuration**:

```json
{
  "hooks": {
    "TodoCreated": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.qwen/hooks/todo-validator.sh",
            "name": "todo-validator",
            "timeout": 5
          }
        ]
      }
    ]
  }
}
```

#### TodoCompleted

**Purpose**: Executed when a todo item is marked as completed. Allows validation, logging, or blocking of todo completion.

Todo hooks run in two phases:

- `validation`: runs before persistence. Use this phase for validation only; returning `block` or `deny` prevents the write.
- `postWrite`: runs after persistence. Use this phase for side effects such as logging or syncing; `block` or `deny` is ignored in this phase.

**Event-specific fields**:

```json
{
  "todo_id": "unique identifier for the todo item",
  "todo_content": "content/description of the todo item",
  "previous_status": "pending | in_progress (status before completion)",
  "all_todos": "array of all todo items in the current list",
  "phase": "validation | postWrite"
}
```

**Output Options**:

- `decision`: "allow", "block", or "deny"
- `reason`: human-readable explanation for the decision (required when blocking)

**Blocking Behavior**:

During the `validation` phase, when `decision` is `block` or `deny` (exit code 2), todo completion is prevented. The todo item remains in its previous status, and the reason is provided as feedback to the model.

During the `postWrite` phase, the todo has already been persisted. Hooks may still return output, but `block` / `deny` does not undo the write and should not be used for validation.

**Example Output (Allow)**:

```json
{
  "decision": "allow",
  "reason": "Todo completion approved"
}
```

**Example Output (Block)**:

```json
{
  "decision": "block",
  "reason": "Cannot complete this todo until dependent tasks are finished."
}
```

**Example Hook Script**:

```bash
#!/bin/bash
# ~/.qwen/hooks/todo-completion-validator.sh
# Validates todo completion conditions

INPUT=$(cat)
TODO_ID=$(echo "$INPUT" | jq -r '.todo_id')
ALL_TODOS=$(echo "$INPUT" | jq -r '.all_todos')

# Check if there are incomplete dependent todos (example logic)
INCOMPLETE_COUNT=$(echo "$ALL_TODOS" | jq '[.[] | select(.status != "completed")] | length')

if [ "$INCOMPLETE_COUNT" -gt 5 ]; then
  echo '{"decision": "block", "reason": "Too many incomplete todos. Complete other tasks first."}'
  exit 2
fi

echo '{"decision": "allow"}'
exit 0
```

**Example Configuration**:

```json
{
  "hooks": {
    "TodoCompleted": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.qwen/hooks/todo-completion-validator.sh",
            "name": "completion-validator",
            "timeout": 5
          }
        ]
      }
    ]
  }
}
```

**Use Cases**:

- **Logging**: Track todo creation and completion for audit or analytics
- **Validation**: Enforce content quality standards (minimum length, required keywords)
- **Workflow Control**: Block completion until prerequisites are met
- **Integration**: Sync todos with external task management systems (Jira, Trello, etc.)

#### InstructionsLoaded

**Purpose**: Runs for each context file Qwen loads into the system prompt, such as `QWEN.md`, and for each file those files import. Use it to audit which instruction files a session uses. It is informational: it cannot stop or change loading.

It fires when the session starts, when context files are reloaded during the session, and for every file pulled in through an import, after the file has been read. Qwen waits for the hooks before it continues loading; a hook failure is logged and does not stop loading.

**Matcher**: Matches against `file_path`. For example, `"matcher": "QWEN\\.local\\.md$"` fires only for local context files.

**Event-specific fields**:

```json
{
  "file_path": "path of the loaded file",
  "memory_type": "user | project | local | extension",
  "load_reason": "session_start | include | refresh",
  "trigger_file_path": "for include: the context file whose loading pulled this file in (optional)",
  "parent_file_path": "for include: the file that contains the import (optional)"
}
```

`load_reason` is `session_start` for the initial load, `refresh` when context files are reloaded, and `include` for a file reached through an import.

`memory_type` is `extension` for an extension's context files and files in its directory, `user` for files in the global `.qwen` directory or directly in your home directory, `local` for `.qwen/QWEN.local.md` at the project root, and `project` for any other file. An imported file has the same `memory_type` as the context file that imported it.

**Output Options**:

- **None** - InstructionsLoaded output and exit codes are ignored.

## Hook Configuration

Hooks are configured in Qwen Code settings, typically in `.qwen/settings.json` or user configuration files. Hooks in the system settings files (System and SystemDefaults) load with the source System and, like user hooks, regardless of folder trust. Within one event, sequential hooks from settings and extensions run in this order: Project, User, System, Extension.

> [!NOTE]
> A hook must include a name field to be enabled or disabled individually at runtime. Hooks without a name will always execute when their event and matcher conditions are met.

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^run_shell_command$",
        "sequential": false,
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/security-check.sh",
            "name": "security-check",
            "description": "Run security checks before tool execution",
            "timeout": 30
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Session started'",
            "name": "session-init"
          }
        ]
      }
    ]
  }
}
```

## Hook Execution

### Parallel vs Sequential Execution

- By default, hooks execute in parallel for better performance
- Use `sequential: true` in hook definition to enforce order-dependent execution
- Sequential hooks can modify input for subsequent hooks in the chain

### Async Hooks

Only `command` type supports asynchronous execution. Setting `"async": true` runs the hook in the background without blocking the main flow.

Async hooks are scoped to the Qwen process. On POSIX, Qwen reclaims a still-running async hook process tree when it exits, except for event types whose sections explicitly guarantee fire-and-forget completion after exit. Windows cannot reconstruct a descendant tree after its root exits, so full parent-exit reclamation there requires a Job Object or descendant tracking.

**Features:**

- Cannot return decision control (the event continues as soon as the hook starts)
- Output is not delivered yet: `systemMessage`, `additionalContext` and plain `stdout` from an async hook are neither shown to the user nor added to the model context. The debug log records only that the hook started, completed or failed. Delivering this output is planned.
- Suitable for side effects such as auditing, logging and background tests that write their results somewhere you can read them
- Occupies one of 10 concurrent async hook slots until it finishes or reaches its `timeout` (60 seconds by default). While all 10 slots are in use, a new async hook is skipped

**Example:**

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "write_file|edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$QWEN_PROJECT_DIR/.qwen/hooks/run-tests-async.sh\"",
            "async": true,
            "timeout": 300
          }
        ]
      }
    ]
  }
}
```

```bash
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.js ]]; then exit 0; fi
# Async hook output is not shown yet, so write the result to a file.
LOG=/tmp/qwen-async-tests.log
if npm test >"$LOG" 2>&1; then
  echo "Tests passed after editing $FILE_PATH" >>"$LOG"
else
  echo "Tests failed after editing $FILE_PATH" >>"$LOG"
fi
```

### Security Model

- Hooks run in the user's environment with user privileges
- Project-level hooks require trusted folder status
- Timeouts prevent hanging hooks (default: 60 seconds for command hooks)

## Best Practices

### Example 1: Security Validation Hook

A PreToolUse hook that logs and potentially blocks dangerous commands:

**security_check.sh**

```bash
#!/bin/bash

# Read input from stdin
INPUT=$(cat)

# Parse the input to extract tool info
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input')

# Check for potentially dangerous operations
if echo "$TOOL_INPUT" | grep -qiE "(rm.*-rf|mv.*\/|chmod.*777)"; then
  echo '{
    "hookSpecificOutput": {
      "hookEventName": "PreToolUse",
      "permissionDecision": "deny",
      "permissionDecisionReason": "Security policy blocks dangerous command"
    }
  }'
  exit 2  # Blocking error
fi

# Log the operation
echo "INFO: Tool $TOOL_NAME executed safely at $(date)" >> /var/log/qwen-security.log

# Allow with additional context
echo '{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Security check passed",
    "additionalContext": "Command approved by security policy"
  }
}'
exit 0
```

Configure in `.qwen/settings.json`:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${SECURITY_CHECK_SCRIPT}",
            "name": "security-checker",
            "description": "Security validation for bash commands",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

### Example 2: HTTP Audit Hook

A PostToolUse HTTP hook that sends all tool execution records to a remote audit service:

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "http",
            "url": "https://audit.example.com/api/tool-execution",
            "headers": {
              "Authorization": "Bearer ${AUDIT_API_TOKEN}",
              "Content-Type": "application/json"
            },
            "allowedEnvVars": ["AUDIT_API_TOKEN"],
            "timeout": 10,
            "name": "audit-logger"
          }
        ]
      }
    ]
  }
}
```

### Example 3: Submitted Prompt Validation Hook

On the core/headless path, `prompt` can include generated or expanded content rather than original user input. On ACP it starts with the pre-expansion request text, so reading it does not inspect attachment bodies or the complete model input. `UserPromptSubmit` does not cover every model send. Do not silently fall back from `submitted_prompt` to `prompt` when source provenance is required.

A UserPromptSubmit hook that validates supported submitted text and provides context for long prompts. It also runs on headless submissions and explicitly declared ACP/daemon submissions; it is not TUI-only. It skips invocations where source provenance is unavailable. A blocking result stops the affected invocation, including on these non-TUI paths. The keyword check is illustrative and is not a complete DLP policy:

**prompt_validator.py**

```python
import json
import sys
import re

# Load input from stdin
try:
    input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
    print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
    sys.exit(1)

user_prompt = input_data.get("submitted_prompt")
if user_prompt is None:
    # Do not mistake model-bound or machine-generated content for raw input.
    sys.exit(0)

# Sensitive words list
sensitive_words = ["password", "secret", "token", "api_key"]

# Check for sensitive information
for word in sensitive_words:
    if re.search(rf"\b{word}\b", user_prompt.lower()):
        # Block prompts containing sensitive information
        output = {
            "decision": "block",
            "reason": f"Prompt contains sensitive information '{word}'. Please remove sensitive content and resubmit.",
            "hookSpecificOutput": {
                "hookEventName": "UserPromptSubmit"
            }
        }
        print(json.dumps(output))
        sys.exit(0)

# Check prompt length and add warning context if too long
if len(user_prompt) > 1000:
    output = {
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": "Note: User submitted a long prompt. Please read carefully and ensure all requirements are understood."
        }
    }
    print(json.dumps(output))
    sys.exit(0)

# No processing needed for normal cases
sys.exit(0)
```

## Troubleshooting

### A hook does not fire

1. **Event name.** The key must be one of the events in [Hook Events](#hook-events), spelled exactly, including case. An unknown event name is skipped with the warning `Invalid hook event name`.
2. **Matcher.** Tool events match the runtime tool id, such as `run_shell_command` or `write_file`, or an accepted alias such as Claude Code's `Bash` or `Write` (see [Matcher Patterns](#matcher-patterns)). On events without matcher support, `matcher` is ignored.
3. **Folder trust.** Hooks in a project's `.qwen/settings.json` load only when the folder is trusted. User hooks load regardless of folder trust.
4. **Hooks disabled.** `"disableAllHooks": true`, `--safe-mode` (or `QWEN_CODE_SAFE_MODE`) and `--bare` (or `QWEN_CODE_SIMPLE`) turn off all hooks, including user hooks.
5. **Debug log.** Start Qwen Code with `--debug`, or set `QWEN_DEBUG_LOG_FILE=1`, then read the session log at `~/.qwen/debug/latest` (under `$QWEN_RUNTIME_DIR` when that is set). Each line is tagged with a namespace:
   - `[HOOK_REGISTRY]`: `Hook registry initialized with N hook entries`, and hook definitions discarded as invalid
   - `[TRUSTED_HOOKS]`: `Hook <name> started for event <event>` and the matching `ended` line for each run, async hook status, and hook system messages
   - `[HOOK_MATCHER]`: matchers that are not valid regular expressions
   - `[HOOK_TIMEOUT]`: command hook `timeout` values read as milliseconds or ignored
   - `[HTTP_HOOK_RUNNER]`, `[URL_VALIDATOR]`, `[PROMPT_HOOK_RUNNER]`, `[FUNCTION_HOOK_RUNNER]`, `[SKILL_HOOKS]`, `[SESSION_HOOKS_MANAGER]`, `[ASYNC_HOOK_REGISTRY]` and `[HOOK_AGGREGATOR]`: details from the individual runners and registries

   Prompt-hook inputs can be written to the session debug log, so apply appropriate access and retention controls.

6. **Edited during the session.** Hooks are read when the session starts. After editing hook definitions, open the interactive `/hooks` menu once to reload them, or restart Qwen Code. Changes to hook controls and HTTP security settings require a restart.

### Other checks

- Verify hook script permissions and executability
- Ensure proper JSON formatting in hook outputs
- Use specific matcher patterns to avoid unintended hook execution
- Temporarily disable all hooks: add `"disableAllHooks": true` in settings

---

### Users Features Language (`docs/users/features/language.md`)

# Internationalization (i18n) & Language

Qwen Code is built for multilingual workflows: it supports UI localization (i18n/l10n) in the CLI, lets you choose the assistant output language, and allows custom UI language packs.

## Overview

From a user point of view, Qwen Code’s “internationalization” spans multiple layers:

| Capability / Setting     | What it controls                                                       | Where stored                 |
| ------------------------ | ---------------------------------------------------------------------- | ---------------------------- |
| `/language ui`           | Terminal UI text (menus, system messages, prompts)                     | `~/.qwen/settings.json`      |
| `/language output`       | Language the AI responds in (an output preference, not UI translation) | `~/.qwen/output-language.md` |
| Custom UI language packs | Overrides/extends built-in UI translations                             | `~/.qwen/locales/*.js`       |

## UI Language

This is the CLI’s UI localization layer (i18n/l10n): it controls the language of menus, prompts, and system messages.

### Setting the UI Language

Use the `/language ui` command:

```bash
/language ui zh-CN    # Chinese
/language ui en-US    # English
/language ui ru-RU    # Russian
/language ui de-DE    # German
/language ui ja-JP    # Japanese
/language ui pt-BR    # Portuguese (Brazil)
/language ui fr-FR    # French
/language ui ca-ES    # Catalan
```

Aliases are also supported:

```bash
/language ui zh       # Chinese
/language ui en       # English
/language ui ru       # Russian
/language ui de       # German
/language ui ja       # Japanese
/language ui pt       # Portuguese
/language ui fr       # French
/language ui ca       # Catalan
```

### Auto-detection

On first startup, Qwen Code detects your system locale and sets the UI language automatically.

Detection priority:

1. `QWEN_CODE_LANG` environment variable
2. `LANG` environment variable
3. System locale via JavaScript Intl API
4. Default: English

## LLM Output Language

The LLM output language controls what language the AI assistant responds in, regardless of what language you type your questions in.

### How It Works

The LLM output language is controlled by a rule file at `~/.qwen/output-language.md`. This file is automatically included in the LLM's context during startup, instructing it to respond in the specified language.

### Auto-detection

On first startup, if no `output-language.md` file exists, Qwen Code automatically creates one based on your system locale. For example:

- System locale `zh` creates a rule for Chinese responses
- System locale `en` creates a rule for English responses
- System locale `ru` creates a rule for Russian responses
- System locale `de` creates a rule for German responses
- System locale `ja` creates a rule for Japanese responses
- System locale `pt` creates a rule for Portuguese responses
- System locale `fr` creates a rule for French responses
- System locale `ca` creates a rule for Catalan responses

### Manual Setting

Use `/language output <language>` to change:

```bash
/language output Chinese
/language output English
/language output Japanese
/language output German
```

Any language name works. The LLM will be instructed to respond in that language.

> [!note]
>
> After changing the output language, restart Qwen Code for the change to take effect.

### File Location

```
~/.qwen/output-language.md
```

## Configuration

### Via Settings Dialog

1. Run `/settings`
2. Find "Language" under General
3. Select your preferred UI language

### Via Environment Variable

```bash
export QWEN_CODE_LANG=zh
```

This influences auto-detection on first startup (if you haven’t set a UI language and no `output-language.md` file exists yet).

## Custom Language Packs

For UI translations, you can create custom language packs in `~/.qwen/locales/`:

- Example: `~/.qwen/locales/es.js` for Spanish
- Example: `~/.qwen/locales/fr.js` for French

User directory takes precedence over built-in translations.

> [!tip]
>
> Contributions are welcome! If you’d like to improve built-in translations or add new languages.
> For a concrete example, see [PR #1238: feat(i18n): add Russian language support](https://github.com/QwenLM/qwen-code/pull/1238).

### Maintaining `zh-TW` (Traditional Chinese for Taiwan)

`zh-TW` is **not** an automatic OpenCC s2t conversion of `zh.js` — it is a hand-maintained Taiwan-vocabulary translation. When adding or updating keys, please follow the conventions below.

The "CI enforced?" column indicates whether `npm run check-i18n` will fail the build on a violation. Rows marked **No** are style guidance enforced by review only — typically because the offending form has a legitimate non-UI meaning (`文件` can mean "document", `打開` is colloquially fine in Taiwan).

| Avoid                 | Use instead           | CI enforced? | Reason                                                                                                                                                                           |
| --------------------- | --------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 文件 (file)           | 檔案                  | No           | Taiwan term for filesystem files (but `文件` can legitimately mean "document")                                                                                                   |
| 服務器 / 服务器       | 伺服器                | Yes          | Taiwan term for "server"                                                                                                                                                         |
| 菜單 / 菜单           | 選單                  | Yes          | Taiwan term for "menu"                                                                                                                                                           |
| 鏈接 / 链接           | 連結                  | Yes          | Taiwan term for "link" (bare `鏈` is fine — e.g. 區塊鏈)                                                                                                                         |
| 打開                  | 開啟                  | No           | Taiwan-preferred verb for "open" (UI); `打開` is colloquially common                                                                                                             |
| 爲 / 啓 / 曆史 / 鏈接 | 為 / 啟 / 歷史 / 連結 | Yes          | Variant Traditional forms from raw OpenCC s2t. Note: `曆` is context-dependent and correct in calendar terms (日曆, 農曆, 西曆); CI only flags the bigram `曆史`, not bare `曆`. |

If you are not a Traditional Chinese speaker and need to bootstrap a value, **do not paste raw OpenCC `s2t` output**: the default s2t profile emits variant Traditional characters (e.g. 爲, 啓) that Taiwan does not use, and never rewrites Mainland-Chinese vocabulary (服務器, 菜單). Prefer `s2twp.json` (Simplified → Taiwan with phrase mapping) as a starting point and then ask a Taiwan-Chinese speaker to review.

The `check-i18n` script (run in CI via `npm run check-i18n`) will fail the build if any of the CI-enforced substrings above end up in a `zh-TW` value. See `scripts/check-i18n.ts → ZH_TW_FORBIDDEN_PATTERNS` for the full list. If a translation legitimately needs to contain a CI-forbidden substring, add its key to `ZH_TW_ALLOWED_EXCEPTIONS` in the same file with a brief justification.

> [!note]
>
> The check uses plain substring matching, which does not understand Chinese word boundaries. A bigram pattern can therefore false-positive across compound-word boundaries — for example, `區塊鏈接口` (= `區塊鏈` + `接口`) contains the substring `鏈接` even though neither word is incorrect. If you hit a surprising CI failure of this kind, add the translation key to `ZH_TW_ALLOWED_EXCEPTIONS` rather than removing the pattern.

### Language Pack Format

```javascript
// ~/.qwen/locales/es.js
export default {
  Hello: 'Hola',
  Settings: 'Configuracion',
  // ... more translations
};
```

## Related Commands

- `/language` - Show current language settings
- `/language ui [lang]` - Set UI language
- `/language output <language>` - Set LLM output language
- `/settings` - Open settings dialog

---

### Users Features Lsp (`docs/users/features/lsp.md`)

# Language Server Protocol (LSP) Support

Qwen Code provides native Language Server Protocol (LSP) support, enabling advanced code intelligence features like go-to-definition, find references, diagnostics, and code actions. This integration allows the AI agent to understand your code more deeply and provide more accurate assistance.

## Overview

LSP support in Qwen Code works by connecting to language servers that understand your code. Once you configure servers via `.lsp.json` (or extensions), Qwen Code can start them and use them to:

- Navigate to symbol definitions
- Find all references to a symbol
- Get hover information (documentation, type info)
- View diagnostic messages (errors, warnings)
- Access code actions (quick fixes, refactorings)
- Analyze call hierarchies

## Quick Start

LSP is an experimental feature in Qwen Code. To enable it, use the `--experimental-lsp` command line flag:

```bash
qwen --experimental-lsp
```

LSP servers are configuration-driven. You must define them in `.lsp.json` (or via extensions) for Qwen Code to start them.

### Prerequisites

You need to have the language server for your programming language installed:

| Language              | Language Server            | Install Command                                                                |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------ |
| TypeScript/JavaScript | typescript-language-server | `npm install typescript-language-server typescript`                         |
| Python                | pylsp                      | `pip install python-lsp-server`                                                |
| Go                    | gopls                      | `go install golang.org/x/tools/gopls@latest`                                   |
| Rust                  | rust-analyzer              | [Installation guide](https://rust-analyzer.github.io/manual.html#installation) |
| C/C++                 | clangd                     | Install LLVM/clangd via your package manager                                   |
| Java                  | jdtls                      | Install JDTLS and a JDK                                                        |

## Configuration

### .lsp.json File

You can configure language servers using a `.lsp.json` file in your project root. Each top-level key is a language identifier, and its value is the server configuration object.

**Basic format:**

```json
{
  "typescript": {
    "command": "typescript-language-server",
    "args": ["--stdio"],
    "extensionToLanguage": {
      ".ts": "typescript",
      ".tsx": "typescriptreact",
      ".js": "javascript",
      ".jsx": "javascriptreact"
    }
  }
}
```

### C/C++ (clangd) configuration

Dependencies:

- clangd (LLVM) must be installed and available in PATH.
- A compile database (`compile_commands.json`) or `compile_flags.txt` is required for accurate results.

Example:

```json
{
  "cpp": {
    "command": "clangd",
    "args": [
      "--background-index",
      "--clang-tidy",
      "--header-insertion=iwyu",
      "--completion-style=detailed"
    ]
  }
}
```

### Java (jdtls) configuration

Dependencies:

- JDK installed and available in PATH (`java`).
- JDTLS installed and available in PATH (`jdtls`).

Example:

```json
{
  "java": {
    "command": "jdtls",
    "args": ["-configuration", ".jdtls-config", "-data", ".jdtls-workspace"]
  }
}
```

### Configuration Options

#### Required Fields

| Option    | Type   | Description                                                                                                                                       |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `command` | string | Command to start the LSP server. Supports bare command names resolved via `PATH` (e.g. `clangd`) and absolute paths (e.g. `/opt/llvm/bin/clangd`) |

#### Optional Fields

| Option                  | Type     | Default   | Description                                             |
| ----------------------- | -------- | --------- | ------------------------------------------------------- |
| `args`                  | string[] | `[]`      | Command line arguments                                  |
| `transport`             | string   | `"stdio"` | Transport type: `stdio`, `tcp`, or `socket`             |
| `env`                   | object   | -         | Environment variables                                   |
| `initializationOptions` | object   | -         | LSP initialization options                              |
| `settings`              | object   | -         | Server settings via `workspace/didChangeConfiguration`  |
| `extensionToLanguage`   | object   | -         | Maps file extensions to language identifiers            |
| `workspaceFolder`       | string   | -         | Override workspace folder (must be within project root) |
| `startupTimeout`        | number   | `10000`   | Startup timeout in milliseconds                         |
| `shutdownTimeout`       | number   | `5000`    | Shutdown timeout in milliseconds                        |
| `restartOnCrash`        | boolean  | `false`   | Auto-restart on crash                                   |
| `maxRestarts`           | number   | `3`       | Maximum restart attempts                                |
| `trustRequired`         | boolean  | `true`    | Require trusted workspace                               |

### TCP/Socket Transport

For servers that use TCP or Unix socket transport:

```json
{
  "remote-lsp": {
    "transport": "tcp",
    "socket": {
      "host": "127.0.0.1",
      "port": 9999
    },
    "extensionToLanguage": {
      ".custom": "custom"
    }
  }
}
```

## Available LSP Operations

Qwen Code exposes LSP functionality through the unified `lsp` tool. Here are the available operations:

Location-based operations (`goToDefinition`, `findReferences`, `hover`, `goToImplementation`, and `prepareCallHierarchy`) require an exact `filePath` + `line` + `character` position. If you do not know the exact position, use `workspaceSymbol` or `documentSymbol` first to locate the symbol.

### Code Navigation

#### Go to Definition

Find where a symbol is defined.

```
Operation: goToDefinition
Parameters:
  - filePath: Path to the file
  - line: Line number (1-based)
  - character: Column number (1-based)
```

#### Find References

Find all references to a symbol.

```
Operation: findReferences
Parameters:
  - filePath: Path to the file
  - line: Line number (1-based)
  - character: Column number (1-based)
  - includeDeclaration: Include the declaration itself (optional)
```

#### Go to Implementation

Find implementations of an interface or abstract method.

```
Operation: goToImplementation
Parameters:
  - filePath: Path to the file
  - line: Line number (1-based)
  - character: Column number (1-based)
```

### Symbol Information

#### Hover

Get documentation and type information for a symbol.

```
Operation: hover
Parameters:
  - filePath: Path to the file
  - line: Line number (1-based)
  - character: Column number (1-based)
```

#### Document Symbols

Get all symbols in a document.

```
Operation: documentSymbol
Parameters:
  - filePath: Path to the file
```

#### Workspace Symbol Search

Search for symbols across the workspace.

```
Operation: workspaceSymbol
Parameters:
  - query: Search query string
  - limit: Maximum results (optional)
```

### Call Hierarchy

#### Prepare Call Hierarchy

Get the call hierarchy item at a position.

```
Operation: prepareCallHierarchy
Parameters:
  - filePath: Path to the file
  - line: Line number (1-based)
  - character: Column number (1-based)
```

#### Incoming Calls

Find all functions that call the given function.

```
Operation: incomingCalls
Parameters:
  - callHierarchyItem: Item from prepareCallHierarchy
```

#### Outgoing Calls

Find all functions called by the given function.

```
Operation: outgoingCalls
Parameters:
  - callHierarchyItem: Item from prepareCallHierarchy
```

### Diagnostics

#### File Diagnostics

Get diagnostic messages (errors, warnings) for a file.

```
Operation: diagnostics
Parameters:
  - filePath: Path to the file
```

#### Workspace Diagnostics

Get all diagnostic messages across the workspace.

```
Operation: workspaceDiagnostics
Parameters:
  - limit: Maximum results (optional)
```

### Code Actions

#### Get Code Actions

Get available code actions (quick fixes, refactorings) at a location.

```
Operation: codeActions
Parameters:
  - filePath: Path to the file
  - line: Start line number (1-based)
  - character: Start column number (1-based)
  - endLine: End line number (optional, defaults to line)
  - endCharacter: End column (optional, defaults to character)
  - diagnostics: Diagnostics to get actions for (optional)
  - codeActionKinds: Filter by action kind (optional)
```

Code action kinds:

- `quickfix` - Quick fixes for errors/warnings
- `refactor` - Refactoring operations
- `refactor.extract` - Extract to function/variable
- `refactor.inline` - Inline function/variable
- `source` - Source code actions
- `source.organizeImports` - Organize imports
- `source.fixAll` - Fix all auto-fixable issues

## Security

LSP servers are only started in trusted workspaces by default. This is because language servers run with your user permissions and can execute code.

### Trust Controls

- **Trusted Workspace**: LSP servers start if configured
- **Untrusted Workspace**: LSP servers won't start unless `trustRequired: false` is set in the server configuration

To mark a workspace as trusted, use the `/trust` command.

### Per-Server Trust Override

You can override trust requirements for specific servers in their configuration:

```json
{
  "safe-server": {
    "command": "safe-language-server",
    "args": ["--stdio"],
    "trustRequired": false,
    "extensionToLanguage": {
      ".safe": "safe"
    }
  }
}
```

## Troubleshooting

### Server Not Starting

1. **Verify `--experimental-lsp` flag**: Make sure you're using the flag when starting Qwen Code
2. **Check if the server is installed**: Run the command manually (e.g. `clangd --version`) to verify
3. **Check the command**: The server binary must be in your system `PATH`, or specified as an absolute path (e.g. `/opt/llvm/bin/clangd`). Relative paths that escape the workspace are blocked
4. **Check workspace trust**: The workspace must be trusted for LSP (use `/trust`)
5. **Check logs**: Start Qwen Code with `--debug`, then search for LSP-related entries in the debug log (see Debugging section below)
6. **Check the process**: Run `ps aux | grep <server-name>` to verify the server process is running

### Slow Performance

1. **Large projects**: Consider excluding `node_modules` and other large directories
2. **Server timeout**: Increase `startupTimeout` in server configuration for slow servers

### No Results

1. **Server not ready**: The server may still be indexing. For C/C++ projects with clangd, ensure `--background-index` is in the args and a `compile_commands.json` (or `compile_flags.txt`) exists in the project root or a parent directory. Use `--compile-commands-dir=<path>` if it is in a build subdirectory
2. **File not saved**: Save your file for the server to pick up changes
3. **Wrong language**: Check if the correct server is running for your language
4. **Check the process**: Run `ps aux | grep <server-name>` to verify the server is actually running

### Debugging

LSP does not have a separate debug flag. Use Qwen Code's normal debug mode together with the LSP feature flag:

```bash
qwen --experimental-lsp --debug
```

Debug logs are written to the session debug log directory. To check LSP-related entries:

```bash
# Default runtime directory
rg "LSP|Native LSP|clangd|connection closed" ~/.qwen/debug/latest
# Or, without ripgrep:
grep -E "LSP|Native LSP|clangd|connection closed" ~/.qwen/debug/latest

# If QWEN_RUNTIME_DIR is configured
rg "LSP|Native LSP|clangd|connection closed" "$QWEN_RUNTIME_DIR/debug/latest"
```

Useful entries include:

- `[LSP] ...`: Logs emitted by the native LSP service and server manager.
- `[CONFIG] Native LSP status after discovery: ...`: LSP server configuration discovered for the session.
- `[CONFIG] Native LSP status after startup: ...`: Server startup result, including ready/failed counts.
- `[STATUS] LSP status snapshot for /status: ...`: Status snapshot printed when running `/status` in debug mode.

You can also run `/status` in the CLI to see a short LSP summary:

```text
LSP: disabled
LSP: enabled, 1/1 ready
LSP: enabled, 0/1 ready (1 failed)
LSP: enabled, no servers configured
LSP: enabled, status unavailable
```

For per-server details, run `/lsp`:

```text
**LSP Server Status**

| Server | Command | Languages | Status |
|--------|---------|-----------|--------|
| clangd | `clangd` | c, cpp | READY |
| pyright | `pyright-langserver` | python | FAILED - startup failed |
```

Common error messages to look for:

```text
command path is unsafe        -> relative path escapes workspace, use absolute path or add to PATH
command not found             -> server binary not installed or not in PATH
requires trusted workspace    -> run /trust first
LSP connection closed         -> server started but exited or closed stdio before replying to initialize
```

For clangd startup failures, verify the server directly from the project root:

```bash
clangd --version
clangd --check=/path/to/file.cpp --log=verbose
```

C/C++ projects should usually provide a `compile_commands.json` or `compile_flags.txt`. If the compile database is in a build directory, pass it to clangd:

```json
{
  "cpp": {
    "command": "clangd",
    "args": ["--background-index", "--compile-commands-dir=build"]
  }
}
```

```bash
ps aux | grep clangd   # or typescript-language-server, jdtls, etc.
```

## Extension LSP Configuration

Extensions can provide LSP server configurations through the `lspServers` field in their `plugin.json`. This can be either an inline object or a path to a `.lsp.json` file. Qwen Code loads these configs when the extension is enabled. The format is the same language-keyed layout used in project `.lsp.json` files.

## Best Practices

1. **Install language servers globally**: This ensures they're available in all projects
2. **Use project-specific settings**: Configure server options per project when needed via `.lsp.json`
3. **Keep servers updated**: Update your language servers regularly for best results
4. **Trust wisely**: Only trust workspaces from trusted sources

## FAQ

### Q: How do I enable LSP?

Use the `--experimental-lsp` flag when starting Qwen Code:

```bash
qwen --experimental-lsp
```

### Q: How do I know which language servers are running?

Start Qwen Code with LSP and debug mode enabled:

```bash
qwen --experimental-lsp --debug
```

Then run `/status` for a short summary, `/lsp` for per-server status, or inspect the debug log:

```bash
# Default runtime directory
rg "LSP|Native LSP|<server-name>" ~/.qwen/debug/latest
# Or:
grep -E "LSP|Native LSP|<server-name>" ~/.qwen/debug/latest

# If QWEN_RUNTIME_DIR is configured
rg "LSP|Native LSP|<server-name>" "$QWEN_RUNTIME_DIR/debug/latest"
```

LSP uses Qwen Code's normal `--debug` mode; there is no separate LSP debug flag.

### Q: Can I use multiple language servers for the same file type?

Yes, but only one will be used for each operation. The first server that returns results wins.

### Q: Does LSP work in sandbox mode?

LSP servers run outside the sandbox to access your code. They're subject to workspace trust controls.

---

### Users Features Mcp (`docs/users/features/mcp.md`)

# Connect Qwen Code to tools via MCP

Qwen Code can connect to external tools and data sources through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction). MCP servers give Qwen Code access to your tools, databases, and APIs.

## What you can do with MCP

With MCP servers connected, you can ask Qwen Code to:

- Work with files and repos (read/search/write, depending on the tools you enable)
- Query databases (schema inspection, queries, reporting)
- Integrate internal services (wrap your APIs as MCP tools)
- Automate workflows (repeatable tasks exposed as tools/prompts)

> [!tip]
>
> If you’re looking for the “one command to get started”, jump to [Quick start](#quick-start).

## Quick start

Qwen Code loads MCP servers from `mcpServers` in your `settings.json`. You can configure servers either:

- By editing `settings.json` directly
- By using `qwen mcp` commands (see [CLI reference](#manage-mcp-servers-with-qwen-mcp))

### Add your first server

1. Add a server (example: remote HTTP MCP server):

```bash
qwen mcp add --transport http my-server http://localhost:3000/mcp
```

2. Start Qwen Code and open the MCP management dialog to view and manage
   servers:

```bash
qwen
```

Then enter:

```text
/mcp
```

3. If Qwen Code was already running before you added the server, restart it in
   the same project. Then ask the model to use tools from that server.

## Where configuration is stored (scopes)

Most users only need these two scopes:

- **User scope (default)**: `~/.qwen/settings.json` across all projects on your machine
- **Project scope**: `.qwen/settings.json` in your project root

Write to user scope:

```bash
qwen mcp add --scope user --transport http my-server http://localhost:3000/mcp
```

> [!tip]
>
> For advanced configuration layers (system defaults/system settings and precedence rules), see [Settings](../configuration/settings).

## Configure servers

### Choose a transport

| Transport | When to use                                                       | JSON field(s)                               |
| --------- | ----------------------------------------------------------------- | ------------------------------------------- |
| `http`    | Recommended for remote services; works well for cloud MCP servers | `httpUrl` (+ optional `headers`)            |
| `sse`     | Legacy/deprecated servers that only support Server-Sent Events    | `url` (+ optional `headers`)                |
| `stdio`   | Local process (scripts, CLIs, Docker) on your machine             | `command`, `args` (+ optional `cwd`, `env`) |

> [!note]
>
> If a server supports both, prefer **HTTP** over **SSE**.

### Configure via `settings.json` vs `qwen mcp add`

Both approaches produce the same `mcpServers` entries in your `settings.json`—use whichever you prefer.

#### Stdio server (local process)

JSON (`.qwen/settings.json`):

```json
{
  "mcpServers": {
    "pythonTools": {
      "command": "python",
      "args": ["-m", "my_mcp_server", "--port", "8080"],
      "cwd": "./mcp-servers/python",
      "env": {
        "DATABASE_URL": "$DB_CONNECTION_STRING",
        "API_KEY": "${EXTERNAL_API_KEY}"
      },
      "timeout": 15000
    }
  }
}
```

CLI (writes to user scope by default):

```bash
qwen mcp add pythonTools -e DATABASE_URL=$DB_CONNECTION_STRING -e API_KEY=$EXTERNAL_API_KEY \
  --timeout 15000 python -m my_mcp_server --port 8080
```

#### HTTP server (remote streamable HTTP)

JSON:

```json
{
  "mcpServers": {
    "httpServerWithAuth": {
      "httpUrl": "http://localhost:3000/mcp",
      "headers": {
        "Authorization": "Bearer your-api-token"
      },
      "timeout": 5000
    }
  }
}
```

CLI:

```bash
qwen mcp add --transport http httpServerWithAuth http://localhost:3000/mcp \
  --header "Authorization: Bearer your-api-token" --timeout 5000
```

#### SSE server (remote Server-Sent Events)

JSON:

```json
{
  "mcpServers": {
    "sseServer": {
      "url": "http://localhost:8080/sse",
      "timeout": 30000
    }
  }
}
```

CLI:

```bash
qwen mcp add --transport sse sseServer http://localhost:8080/sse --timeout 30000
```

## Using MCP prompts and resources

Besides tools, Qwen Code discovers and surfaces two other MCP primitives.

### Prompts (slash commands)

Any prompt a server advertises via `prompts/list` becomes an executable
**slash command**. After discovery, type `/` and you'll see the prompt
listed (labeled `MCP: <server>`); run it like any other command:

```text
/my_prompt --arg1="value" --arg2="value"
# positional form also works:
/my_prompt "value" "value"
# show the prompt's arguments:
/my_prompt help
```

The prompt's messages are sent to the model, which then acts on them.

> Discovery is lenient about the declared `prompts` capability: some
> servers implement `prompts/list` but omit `prompts` from their
> `initialize` capabilities. Qwen Code attempts `prompts/list` anyway, so
> those prompts still appear. A server that genuinely has no prompts simply
> answers `Method not found`, which is ignored.

### Resources

Resources a server advertises via `resources/list` are discovered per
server. Open the management dialog with `/mcp` and select a server to see
its **Resources** count alongside its tools and prompts. Choose **View
resources** to browse the server's resource URIs; selecting one shows its
description and MIME type along with the exact `@server:uri` reference to
paste into a message. As with prompts, the `resources` capability is not
required to be declared.

Inject a resource's contents into your message with the `@server:uri`
syntax — type `@`, then the server name, a colon, and the resource URI:

```text
summarize @myserver:file:///docs/spec.md and list the open questions
```

Typing `@myserver:` shows an autocomplete list of that server's resources;
keep typing to filter, matching (case-insensitively) either the resource URI
or its friendly name/title. You don't have to know a URI by heart — before
you reach the colon, typing part of a server name also suggests matching
servers that expose resources, so you can pick one and drill straight into
its resource list. On submit, the referenced resource is read and its contents are
appended to your message (text inline, binary blobs as attachments); the
`@server:uri` reference is preserved in the prompt so the model knows what
it is looking at. The `server` prefix must match a configured MCP server —
otherwise the token is treated as a normal file path, so existing
`@path/to/file` references are unaffected. Resource reads are disabled in
untrusted folders.

## Progressive availability and discovery timeouts

Qwen Code discovers MCP servers in the background after the UI is already
interactive. You see the cli's first prompt within a few hundred
milliseconds even when one of your MCP servers takes several seconds
(or never responds), and the model's tool list updates within roughly
one frame (~16 ms) of each server completing its discover handshake.

- **Interactive mode**: the UI appears immediately; an MCP status pill in
  the bottom-right shows `N/M MCP servers ready` while discovery is in
  flight. Sending a prompt before MCP finishes simply means the model
  sees the tools that are ready _at that moment_; subsequent prompts see
  more tools as servers come online.
- **Non-interactive mode** (`--prompt`, stream-json, ACP): the cli still
  waits for MCP discovery to settle before sending the first prompt, so
  scripted / piped invocations see the same complete tool set the
  legacy synchronous behavior produced.

### Per-server `discoveryTimeoutMs`

Each MCP server gets a discovery-only timeout that caps how long the
initial handshake (`connect` + `tools/list` + `prompts/list` +
`resources/list`) is allowed to take. Defaults:

- **stdio servers**: 30 s
- **remote HTTP / SSE servers**: 5 s (network risk is higher)

Override per server when needed:

```jsonc
{
  "mcpServers": {
    "slow-stdio": {
      "command": "node",
      "args": ["./slow-server.js"],
      "discoveryTimeoutMs": 60000,
    },
    "flaky-remote": {
      "httpUrl": "https://example.com/mcp",
      "discoveryTimeoutMs": 10000,
    },
  },
}
```

The existing `timeout` field is **tool-call** timeout (used for each
`tools/call` request, default 10 minutes) and is unaffected by
`discoveryTimeoutMs` — a long-running tool invocation is not a startup
pathology.

### Automatic stdio negotiation

Stdio servers use the single-process legacy initialize flow by default. To
connect to a modern-only stdio server, opt into automatic protocol negotiation:

```jsonc
{
  "mcpServers": {
    "modern-server": {
      "command": "node",
      "args": ["./server.js"],
      "versionNegotiation": "auto",
    },
  },
}
```

Automatic negotiation runs a short-lived copy of the configured server before
starting the session process and can use up to five seconds of the discovery
budget. Keep the default legacy policy for servers with non-idempotent startup
side effects, single-owner locks or PID files, or slow initialize handshakes.

### Rolling back progressive MCP

If you need the old synchronous behavior (cli waits for every MCP server
before showing any UI), set `QWEN_CODE_LEGACY_MCP_BLOCKING=1` in your
environment. This is kept as an escape hatch for at least one release.

## Safety and control

### Trust (skip confirmations)

- **Server trust** (`trust: true`): bypasses confirmation prompts for that server only in a trusted workspace (use sparingly).

### Connection-loss replay

Qwen Code only reconnects and replays the current MCP tool call when the server has `trust: true`, the workspace is trusted, and the tool explicitly declares either `idempotentHint: true` or a consistent read-only annotation. Read-only annotations conflict with `destructiveHint: true` or `idempotentHint: false` and are not replayed.

Calls with missing annotations, conflicting annotations, an untrusted server, or an untrusted workspace are not replayed after a connection failure. Qwen Code reports that the result may be unknown because the server could have completed the operation before the response was lost. Verify the outcome before trying again. This conservative behavior can differ from earlier releases that transparently retried unannotated tools.

Annotations are server-provided behavior hints, not permissions or an authorization boundary. Only configure `trust: true` for servers you control and whose annotations you have verified.

### OAuth authentication

Qwen Code supports OAuth 2.0 authentication for MCP servers. This is useful when accessing remote servers that require authentication.

#### Basic usage

When you add an MCP server with OAuth credentials, Qwen Code will automatically handle the authentication flow:

```bash
qwen mcp add --transport sse oauth-server https://api.example.com/sse/ \
  --oauth-client-id your-client-id \
  --oauth-redirect-uri https://your-server.com/oauth/callback \
  --oauth-authorization-url https://provider.example.com/authorize \
  --oauth-token-url https://provider.example.com/token
```

#### Important: Redirect URI configuration

The OAuth flow requires a redirect URI where the authorization provider sends the authentication code.

- **Local development**: By default, Qwen Code uses `http://localhost:7777/oauth/callback`. This works when running Qwen Code on your local machine with a local browser.

- **Remote/cloud deployments**: When running Qwen Code on remote servers, cloud IDEs, or web terminals, the default `localhost` redirect will NOT work. Configure `--oauth-redirect-uri` with a public URL ending in `/oauth/callback`, then reverse-proxy that path to `http://127.0.0.1:7777/oauth/callback` on the machine running Qwen Code. Qwen Code does not terminate TLS; the proxy must do so.

Example for remote servers:

```bash
qwen mcp add --transport sse remote-server https://api.example.com/sse/ \
  --oauth-redirect-uri https://your-remote-server.example.com/oauth/callback
```

For example, a reverse proxy can forward only this callback path to the local listener:

```nginx
location = /oauth/callback {
  proxy_pass http://127.0.0.1:7777;
}
```

#### Manual configuration via settings.json

You can also configure OAuth by editing `settings.json` directly:

```json
{
  "mcpServers": {
    "oauthServer": {
      "url": "https://api.example.com/sse/",
      "oauth": {
        "enabled": true,
        "clientId": "your-client-id",
        "clientSecret": "your-client-secret",
        "authorizationUrl": "https://provider.example.com/authorize",
        "tokenUrl": "https://provider.example.com/token",
        "redirectUri": "https://your-server.com/oauth/callback",
        "scopes": ["read", "write"]
      }
    }
  }
}
```

OAuth configuration properties:

| Property           | Description                                                                                                           |
| ------------------ | --------------------------------------------------------------------------------------------------------------------- |
| `enabled`          | Enable OAuth for this server (boolean)                                                                                |
| `clientId`         | OAuth client identifier (string, optional with dynamic registration)                                                  |
| `clientSecret`     | OAuth client secret (string, optional for public clients)                                                             |
| `authorizationUrl` | OAuth authorization endpoint (string, auto-discovered if omitted)                                                     |
| `tokenUrl`         | OAuth token endpoint (string, auto-discovered if omitted)                                                             |
| `scopes`           | Required OAuth scopes (array of strings)                                                                              |
| `redirectUri`      | Custom redirect URI (string). **Critical for remote deployments**. Defaults to `http://localhost:7777/oauth/callback` |
| `tokenParamName`   | Query parameter name for tokens in SSE URLs (string)                                                                  |
| `audiences`        | Audiences the token is valid for (array of strings)                                                                   |

#### Token management

OAuth tokens are automatically:

- **Stored** in `~/.qwen/mcp-oauth-tokens.json` (plaintext, mode 0600) by default. If `QWEN_CODE_FORCE_ENCRYPTED_FILE_STORAGE=true` is set, Qwen Code uses keychain-backed storage where available, or `~/.qwen/mcp-oauth-tokens-v2.json` with AES-256-GCM encryption.
- **Refreshed** when expired (if refresh tokens are available)
- **Validated** before each connection attempt

> [!WARNING]
> By default, OAuth tokens are stored unencrypted on disk. On shared or multi-user machines, set `QWEN_CODE_FORCE_ENCRYPTED_FILE_STORAGE=true` to protect credentials.

Use the `/mcp` dialog within Qwen Code to inspect MCP servers and manage
authentication interactively.

### Tool filtering (allow/deny tools per server)

Use `includeTools` / `excludeTools` to restrict tools exposed by a server (from Qwen Code’s perspective).

Example: include only a few tools:

```json
{
  "mcpServers": {
    "filteredServer": {
      "command": "python",
      "args": ["-m", "my_mcp_server"],
      "includeTools": ["safe_tool", "file_reader", "data_processor"],
      "timeout": 30000
    }
  }
}
```

### Global allow/deny lists

The `mcp` object in your `settings.json` defines global rules for all MCP servers:

- `mcp.allowed`: allow-list of MCP server names (keys in `mcpServers`)
- `mcp.excluded`: deny-list of MCP server names

Both lists support glob patterns: `*` matches any sequence of characters and `?` matches a single character (for example, `"*puppeteer*"` matches every server whose name contains `puppeteer`). Entries without glob characters are matched exactly. When a server matches both lists, `mcp.excluded` takes precedence.

Example:

```json
{
  "mcp": {
    "allowed": ["my-trusted-server", "*-internal"],
    "excluded": ["experimental-server"]
  }
}
```

## Troubleshooting

- **Server shows “Disconnected” in `qwen mcp list`**: verify the URL/command is correct, then increase `timeout`.
- **Stdio server fails to start**: use an absolute `command` path, and double-check `cwd`/`env`.
- **Environment variables in JSON don’t resolve**: ensure they exist in the environment where Qwen Code runs (shell vs GUI app environments can differ).

## Reference

### `settings.json` structure

#### Server-specific configuration (`mcpServers`)

Add an `mcpServers` object to your `settings.json` file:

```json
// ... file contains other config objects
{
  "mcpServers": {
    "serverName": {
      "command": "path/to/server",
      "args": ["--arg1", "value1"],
      "env": {
        "API_KEY": "$MY_API_TOKEN"
      },
      "cwd": "./server-directory",
      "timeout": 30000,
      "trust": false
    }
  }
}
```

Configuration properties:

Required (one of the following):

| Property  | Description                                            |
| --------- | ------------------------------------------------------ |
| `command` | Path to the executable for Stdio transport             |
| `url`     | SSE endpoint URL (e.g., `"http://localhost:8080/sse"`) |
| `httpUrl` | HTTP streaming endpoint URL                            |

Optional:

| Property               | Type/Default                                  | Description                                                                                                                                                                                                                                                       |
| ---------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `args`                 | array                                         | Command-line arguments for Stdio transport                                                                                                                                                                                                                        |
| `headers`              | object                                        | Custom HTTP headers when using `url` or `httpUrl`                                                                                                                                                                                                                 |
| `env`                  | object                                        | Environment variables for the server process. Values can reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax                                                                                                                                |
| `cwd`                  | string                                        | Working directory for Stdio transport                                                                                                                                                                                                                             |
| `timeout`              | number<br>(default: 600,000)                  | Request timeout in milliseconds (default: 600,000ms = 10 minutes)                                                                                                                                                                                                 |
| `versionNegotiation`   | `"auto" \| "legacy"`<br>(default: `"legacy"`) | For Stdio servers, `"auto"` opts into protocol negotiation on a disposable sibling process. The default `"legacy"` starts only the session process.                                                                                                               |
| `trust`                | boolean<br>(default: false)                   | When `true`, bypasses tool call confirmations for this server in a trusted workspace (default: `false`)                                                                                                                                                           |
| `includeTools`         | array                                         | List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default.                                       |
| `excludeTools`         | array                                         | List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server.<br>Note: `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded. |
| `targetAudience`       | string                                        | The OAuth Client ID allowlisted on the IAP-protected application you are trying to access. Used with `authProviderType: 'service_account_impersonation'`.                                                                                                         |
| `targetServiceAccount` | string                                        | The email address of the Google Cloud Service Account to impersonate. Used with `authProviderType: 'service_account_impersonation'`.                                                                                                                              |

### MCP App resource limits

MCP Apps can return bundled HTML larger than the default 1 MiB or take longer
than the default 10 seconds to load. Configure only the server that needs more
headroom in `settings.json`:

```json
{
  "mcpServers": {
    "amplitude": {
      "httpUrl": "https://mcp.amplitude.com/mcp",
      "appResourceMaxBytes": 4194304,
      "appResourceTimeoutMs": 120000
    }
  }
}
```

Amplitude still requires OAuth authentication. These settings do not enable
additional App bridge capabilities or guarantee that every App is compatible.

- `appResourceMaxBytes`: maximum decoded HTML size in UTF-8 bytes. Defaults to
  1,048,576 (1 MiB); clamped to 1–4,194,304 bytes (4 MiB).
- `appResourceTimeoutMs`: resource-read deadline, independent of tool execution
  when explicitly set; clamped to 100–120,000 ms. Without an override, the
  deadline remains the smaller of the general `timeout` and 10,000 ms.
- Finite values are rounded down and clamped. Nonnumeric or nonfinite values
  fall back to defaults. Cancellation remains effective at any configured limit.

A limit failure preserves the successful tool result and displays a warning
with the applicable configuration key. Larger HTML stays outside model context,
while increasing retained transcript and replay size. The size check happens
**after** the SDK reads the response; it does not cap network transfer or peak
memory. These two limit settings do not change the sandbox or CSP.

App HTML is mounted only in a daemon-backed WebShell session and in replay of
a recorded transcript. Terminal and headless sessions render the fallback text
instead, so there these keys change only the warning text, the fetch cost and
latency, and the retained transcript size.

Set these keys in the same source that declares the server. Configuration
sources replace whole server objects by precedence rather than merging fields,
so a same-named entry added in another file shadows the original server
instead of adjusting its limits.

<a id="qwen-mcp-cli"></a>

### Manage MCP servers with `qwen mcp`

You can always configure MCP servers by manually editing `settings.json`, but the CLI is usually faster.

#### Adding a server (`qwen mcp add`)

```bash
qwen mcp add [options] <name> <commandOrUrl> [args...]
```

| Argument/Option             | Description                                                         | Default                                | Example                                                            |
| --------------------------- | ------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------ |
| `<name>`                    | A unique name for the server.                                       | —                                      | `example-server`                                                   |
| `<commandOrUrl>`            | The command to execute (for `stdio`) or the URL (for `http`/`sse`). | —                                      | `/usr/bin/python` or `http://localhost:8`                          |
| `[args...]`                 | Optional arguments for a `stdio` command.                           | —                                      | `--port 5000`                                                      |
| `-s`, `--scope`             | Configuration scope (user or project).                              | `user`                                 | `-s user`                                                          |
| `-t`, `--transport`         | Transport type (`stdio`, `sse`, `http`).                            | `stdio`                                | `-t sse`                                                           |
| `-e`, `--env`               | Set environment variables.                                          | —                                      | `-e KEY=value`                                                     |
| `-H`, `--header`            | Set HTTP headers for SSE and HTTP transports.                       | —                                      | `-H "X-Api-Key: abc123"`                                           |
| `--timeout`                 | Set connection timeout in milliseconds.                             | —                                      | `--timeout 30000`                                                  |
| `--trust`                   | Trust the server; skip confirmations in trusted workspaces.         | — (`false`)                            | `--trust`                                                          |
| `--description`             | Set the description for the server.                                 | —                                      | `--description "Local tools"`                                      |
| `--include-tools`           | A comma-separated list of tools to include.                         | all tools included                     | `--include-tools mytool,othertool`                                 |
| `--exclude-tools`           | A comma-separated list of tools to exclude.                         | none                                   | `--exclude-tools mytool`                                           |
| `--oauth-client-id`         | OAuth client ID for MCP server authentication.                      | —                                      | `--oauth-client-id your-client-id`                                 |
| `--oauth-client-secret`     | OAuth client secret for MCP server authentication.                  | —                                      | `--oauth-client-secret your-client-secret`                         |
| `--oauth-redirect-uri`      | OAuth redirect URI for authentication callback.                     | `http://localhost:7777/oauth/callback` | `--oauth-redirect-uri https://your-server.com/oauth/callback`      |
| `--oauth-authorization-url` | OAuth authorization URL.                                            | —                                      | `--oauth-authorization-url https://provider.example.com/authorize` |
| `--oauth-token-url`         | OAuth token URL.                                                    | —                                      | `--oauth-token-url https://provider.example.com/token`             |
| `--oauth-scopes`            | OAuth scopes (comma-separated).                                     | —                                      | `--oauth-scopes scope1,scope2`                                     |

> `--oauth-*` flags apply only to `--transport sse` and `--transport http`. Combining them with `--transport stdio` is rejected.

#### Removing a server (`qwen mcp remove`)

```bash
qwen mcp remove <name>
```

---

### Users Features Memory (`docs/users/features/memory.md`)

# Memory

Every Qwen Code session starts with a fresh context window. Two mechanisms carry knowledge across sessions so you don't have to re-explain yourself every time:

- **QWEN.md** — instructions _you_ write once and Qwen reads every session
- **Auto-memory** — notes Qwen writes itself based on what it learns from you

---

## QWEN.md: your instructions to Qwen

QWEN.md is a plain text file where you write things Qwen should always know about your project or your preferences. Think of it as a permanent briefing that loads at the start of every conversation.

### What to put in QWEN.md

Add things you'd otherwise have to repeat every session:

- Build and test commands (`npm run test`, `make build`)
- Coding conventions your team follows ("all new files must have JSDoc comments")
- Architectural decisions ("we use the repository pattern, never call the database directly from controllers")
- Personal preferences ("always use pnpm, not npm")
- A verification policy for high-stakes work — for example "verify against the database before concluding" (see [Enforce evidence-based conclusions](../common-workflow.md#enforce-evidence-based-conclusions) for a ready-made template)

Don't include things Qwen can figure out by reading your code. QWEN.md works best when it's short and specific — the longer it gets, the less reliably Qwen follows it.

### Where to create QWEN.md

| File                          | Who it applies to                                |
| ----------------------------- | ------------------------------------------------ |
| `~/.qwen/QWEN.md`             | You, across all your projects                    |
| `QWEN.md` in the project root | Your whole team (commit it to source control)    |
| `.qwen/QWEN.local.md`         | Only you, only in this project (keep out of git) |

You can have any combination of these. Qwen loads all of them when you start a session.

If your repository already has an `AGENTS.md` file for other AI tools, Qwen reads that too. No need to duplicate instructions.

#### When to use `.qwen/QWEN.local.md`

Use it for **project-specific but personal** instructions — things that belong to this project but shouldn't be shared with the team:

- Your own cluster ID, container registry namespace, or cloud account
- A personal debug command that hardcodes your local environment
- Notes you want Qwen to know about your work-in-progress, but not commit

It loads **after** the shared project `QWEN.md`, so your local instructions can supplement or override the team's.

**You must gitignore it yourself.** Although `.qwen/` is often treated as a local directory, qwen-code does not generate a `.gitignore` for you, and some projects commit `.qwen/settings.json`. Add this line to your `.gitignore` (or to your global git ignore):

```
.qwen/QWEN.local.md
```

### Generate one automatically with `/init`

Run `/init` and Qwen will analyze your codebase to create a starter QWEN.md with build commands, test instructions, and conventions it finds. If one already exists, it suggests additions instead of overwriting.

### Reference other files

You can point QWEN.md at other files so Qwen reads them too:

```markdown
See @README.md for project overview.

# Conventions

- Git workflow: @docs/git-workflow.md
```

Use `@path/to/file` anywhere in QWEN.md. Relative paths resolve from the QWEN.md file itself.

---

## Auto-memory: what Qwen learns about you

Auto-memory runs in the background. After each of your conversations, Qwen quietly saves useful things it learned — your preferences, feedback you gave, project context — so it can use them in future sessions without you repeating yourself.

This is different from QWEN.md: you don't write it, Qwen does.

### What Qwen saves

Qwen looks for four kinds of things worth remembering:

| What                    | Examples                                                 |
| ----------------------- | -------------------------------------------------------- |
| **About you**           | Your role, background, how you like to work              |
| **Your feedback**       | Corrections you made, approaches you confirmed           |
| **Project context**     | Ongoing work, decisions, goals not obvious from the code |
| **External references** | Dashboards, ticket trackers, docs links you mentioned    |

Qwen doesn't save everything — only things that would actually be useful next time.

### Where it's stored

Auto-memory files live at `~/.qwen/projects/<project>/memory/`. All branches of the same checkout share the same memory folder, so what Qwen learns in one branch is available in others. Each linked git worktree gets its own memory folder, matching the per-worktree isolation of chats and other session state — repository-wide conventions you want in every worktree belong in [team memory](#team-memory-shared-with-collaborators).

Everything saved is plain markdown — you can open, edit, or delete any file at any time.

`MEMORY.md` is a compact index, rather than the full memory store. Its generated body and the portion loaded into prompts share a limit of 200 lines and 25,000 UTF-16 code units; truncation notices are added separately. Line endings are normalized before prompt budgeting. Under size pressure, complete ordinary entries (up to 150 code units) get space before longer entries, and retained entries keep their original order. An entry that cannot fit is skipped whole, so its Markdown link is never cut in the middle. Keep detail in the linked topic files.

#### Pinned memory

Put hand-curated documents that automatic memory maintenance should preserve
under `pinned/` in a managed-memory directory, for example
`~/.qwen/projects/<project>/memory/pinned/architecture.md` or
`~/.qwen/memories/pinned/preferences.md`. Use the same frontmatter as other
memory documents. Valid pinned files are readable by Qwen and are included the
next time `MEMORY.md` is rebuilt, under the same size and file-count limits as
other memory documents.

Only the top-level `pinned/` directory directly inside a managed-memory root is
protected; nested directories such as `memory/project/pinned/` are ordinary
writable memory. Automatic extraction and Dream workers match the reserved
directory name case-insensitively.

Automatic extraction is instructed to leave pinned records and their valid
index entries unchanged, while Dream is instructed to skip `pinned/` during
consolidation. Both automatic extraction and forked Dream workers, including
background cleanup, enforce the pinned-file boundary on their write and edit
tools, including paths that resolve through a symlink into `pinned/`.
Command-line deletion is closed off too: only the project Dream worker holds a
shell, and its policy is read-only, while automatic extraction and the
user-memory Dream worker hold no shell at all. You still control
these files directly and can remove them with an explicit `/forget` request.

> **Note:** The visible `/dream` slash command runs on the main Agent. It
> receives the same skip instruction, but does not yet receive the forked
> worker's deterministic per-turn tool gate.

### Periodic cleanup

Qwen periodically goes through its saved memories to remove duplicates and clean up outdated entries. This runs automatically in the background once a day after enough sessions have accumulated. You can trigger it manually with `/dream` if you want it to run now.

Your session continues normally while cleanup runs in the background.

### Turning it on or off

Auto-memory is on by default. To toggle it, open `/memory` and use the switches at the top. You can turn off just the automatic saving, just the periodic cleanup, or both.

You can also set them in `~/.qwen/settings.json` (applies to all projects) or `.qwen/settings.json` (this project only):

```json
{
  "memory": {
    "enableManagedAutoMemory": true,
    "enableManagedAutoDream": true
  }
}
```

### Structured recall (opt-in)

By default Qwen reads memory as a flat `MEMORY.md` index. The structured protocol replaces that with a hierarchical memory tree, injects only the subtree relevant to your current request, and gives Qwen a `search_memory` tool to pull full entries on demand — which costs fewer tokens once a memory collection grows past a handful of files.

It is off by default. Turn it on in `settings.json` (restart required):

```json
{
  "memory": {
    "enableStructuredRecall": true
  }
}
```

or for a single run with `QWEN_CODE_MEMORY_STRUCTURED_RECALL=1`.

Turning it on starts a background metadata migration: each memory file gains the frontmatter (category, keywords, usage scenarios) that recall needs. It runs in small batches of at most 10 files per turn and only rewrites frontmatter, never the body of a note. While the setting is off the migration is never scheduled, so nothing is rewritten and no background model calls are made.

### Team memory (shared with collaborators)

By default, auto-memory is **private to you** — it lives under your home directory and is never shared. Team memory is an opt-in tier that the whole team shares **through git**.

When enabled, Qwen gains a third memory directory at `.qwen/team-memory/` **inside the repository**. It uses the same one-file-per-memory layout and `MEMORY.md` index as the private tiers. Because it is committed to the repo, it is shared with every collaborator the normal way: you `git pull` to receive teammates' memories and commit/push to share yours. Qwen routes durable, project-wide knowledge here — conventions every contributor must follow, shared reference pointers (trackers, dashboards) — while personal and fast-decaying notes stay private.

Enable it per project (or globally) in `settings.json`:

```json
{
  "memory": {
    "enableTeamMemory": true
  }
}
```

It is **off by default**. Keep these caveats in mind:

- **It is source-controlled and visible to everyone with repo access.** Treat a team memory like committing to the repo.
- **Secrets are blocked.** Writes to `.qwen/team-memory/` are scanned for credentials (API keys, tokens, private keys); a detected secret is rejected, never written. The scan is a backstop, not a guarantee — don't put sensitive data there.
- **Changes are reviewable.** Team memory writes appear in `git status` / the PR diff like any other file, so they can be reviewed before they're committed. In the default approval mode Qwen also asks before each team write; in `AUTO_EDIT`/YOLO mode (where you've opted into auto-approval) they are applied without a prompt but still surface in the diff.
- **The directory must be git-tracked.** If your project's `.gitignore` excludes `.qwen/*`, re-include the path so it can be shared:

  ```gitignore
  !.qwen/team-memory/
  !.qwen/team-memory/**
  ```

  Caveat: use the file-glob ignore form (`.qwen/*`), not a directory form with a trailing slash (`.qwen/`). A directory-form ignore makes git skip the folder entirely, so a `!`-reinclude below it is a no-op and the team tier stays silently empty in git. Qwen warns once at startup when the tier is enabled but its directory is git-ignored or outside any git repository, so this misconfiguration does not pass unnoticed.

`QWEN_CODE_MEMORY_TEAM=1` / `=0` overrides the setting for a single run.

### Automatic git sync (optional)

By default you share team memory with the normal git workflow (`pull` to receive, `commit`/`push` to share). To have Qwen do it for you, enable sync:

```json
{
  "memory": {
    "enableTeamMemory": true,
    "enableTeamMemorySync": true
  }
}
```

When on, at session start Qwen best-effort syncs the `.qwen/team-memory/` directory: it rebuilds the shared `MEMORY.md` index, fast-forward-pulls collaborators' updates **first**, then commits your team-memory changes on top, and pushes **only that sync commit** (via an explicit single-branch refspec) — so the index you load reflects the latest. It only **stages** the team directory (your other working changes are never committed), and never blocks the session on a git failure. Off by default. `QWEN_CODE_MEMORY_TEAM_SYNC=1` / `=0` overrides the setting for a single run.

Two things to know before enabling it:

- **The fast-forward pull acts on your whole current branch, not just `.qwen/team-memory/`** (git has no path-scoped pull). So sync will fast-forward your branch to the remote tip. The push, by contrast, is scoped: it publishes **only the commit this sync just created**, so it never pushes other unpushed commits you have — if your branch is already ahead of upstream, sync commits locally and skips the push. Enable it on branches where the fast-forward pull is fine — or run it on a dedicated checkout.
- **A diverged branch is left untouched** (`--ff-only` never merges). When that happens sync simply does nothing that session; resolve the divergence (`git pull`) and it resumes. A branch with no upstream (no tracking configuration) still commits locally but skips the push — there is nowhere to push to.

---

## Commands

### `/memory`

Opens the Memory panel. From here you can:

- Turn auto-memory saving on or off
- Turn periodic cleanup (dream) on or off
- Open your personal QWEN.md (`~/.qwen/QWEN.md`)
- Open the project QWEN.md
- Browse the auto-memory folder

### `/memory migrate-team`

Migrates the **team memory** tier (`.qwen/team-memory/`) to the structured metadata format: each file gains the frontmatter (name, description, category, keywords, usage scenarios) that memory recall needs. Personal and project memory are migrated automatically in the background once [structured recall](#structured-recall-opt-in) is enabled; team memory is shared through git, so the rewrite runs only on explicit request and shows up as a reviewable commit. Run it once per repository in a trusted workspace with team memory enabled.

### `/init`

Generates a starter QWEN.md for your project. Qwen reads your codebase and fills in build commands, test instructions, and conventions it discovers.

### `/remember <text>`

Immediately saves something to auto-memory without waiting for Qwen to pick it up automatically:

```
/remember always use snake_case for Python variable names
/remember the staging environment is at staging.example.com
```

### `/forget <text>`

Removes auto-memory entries that match your description:

```
/forget old workaround for the login bug
```

### `/dream`

Runs the memory cleanup now instead of waiting for the automatic schedule:

```
/dream
```

---

## Troubleshooting

### Qwen isn't following my QWEN.md

Open `/memory` to see which files are loaded. If your file isn't listed, Qwen can't see it — make sure it's in the project root or `~/.qwen/`.

Instructions work better when they're specific:

- ✓ `Use 2-space indentation for TypeScript files`
- ✗ `Format code nicely`

If you have multiple QWEN.md files with conflicting instructions, Qwen may behave inconsistently. Review them and remove any contradictions.

### I want to see what Qwen has saved

Run `/memory` and select **Open auto-memory folder**. All saved memories are readable markdown files you can browse, edit, or delete.

### Qwen keeps forgetting things

If auto-memory is on but Qwen doesn't seem to remember things across sessions, try running `/dream` to force a cleanup pass. Also check `/memory` to confirm both toggles are enabled.

For things you always want Qwen to remember, add them to QWEN.md instead — auto-memory is best-effort, QWEN.md is guaranteed.

---

### Users Features Sandbox (`docs/users/features/sandbox.md`)

# Sandbox

This document explains tool execution confinement on Linux and the existing whole-CLI sandbox methods.

## Linux tool execution sandbox

On Linux, install `bwrap` (Bubblewrap) at `/usr/bin/bwrap` (the supported system path) for the strongest supported boundary, then enable the tool execution sandbox in your **User** settings (`~/.qwen/settings.json`, or the directory selected by `QWEN_HOME`) or administrator **System** settings:

```json
{
  "tools": {
    "executionSandbox": {
      "backend": "auto",
      "filesystem": "workspace-write",
      "network": "closed"
    }
  }
}
```

`filesystem` and `network` are required. `backend` defaults to `auto`, which tries bwrap first. When bwrap is unavailable and `network` is `open`, `auto` can use the bundled Landlock helper on kernels with Landlock ABI 3 or newer. Set `backend` to `bwrap` or `landlock` to require that exact implementation. Landlock cannot enforce `network: closed` or hide explicit masked paths. Either requirement rejects Landlock before any command starts; automatic fallback never discards a remaining path mask. Use `read-only` to deny workspace writes, or `workspace-write` to permit writes inside the current canonical workspace. Each command also gets private scratch space. Ordinary `includeDirectories` settings do not grant write access. Keep the workspace separate from the Qwen installation, user configuration and runtime state directories.

The CLI, model transport, authentication and session storage remain on the host. Shell, terminal `!`, prompt shell interpolation, Monitor, Read/Write/Edit and supported nested in-process Agent/Code Mode calls use the same runtime policy. With bwrap, `network: closed` blocks ordinary host/external IP connections from commands, while `open` shares the host network. Reads remain broad: this mode does not hide host secrets or promise to isolate pathname Unix sockets and host services. Approval and YOLO do not expand the filesystem or network policy. Standard streams are explicit caller-provided capabilities: an already-connected socket, TTY or character device remains usable even with `network: closed`. Closed networking does not revoke those existing descriptors; avoid supplying a host-service connection as stdin when that access is unwanted. Host-backed file and named-FIFO stdin require the bundled Linux x64/arm64 input helper and are copied through a private Unix stream socket with its write half shut; regular-file offsets advance only by bytes consumed from that socket, and an idle FIFO writer does not delay command completion. Read fd 0 directly: this input is a socket rather than a FIFO, and reopening it through `/dev/stdin` or `/proc/self/fd/0` is unsupported. SIGINT/SIGTERM cancel the command but still drain accepted stdout/stderr bytes; a closed output stops that stream with exit 141 while the other valid output drains.

Landlock reports `partial` enforcement because it restricts pathname reads, writes and directory mutations but does not create PID or network namespaces and cannot currently restrict every metadata operation, including `chmod`, `chown`, extended attributes and timestamps. A Landlock command can see host processes and uses host networking. Detached descendants retain the Landlock filesystem restrictions but do not get bwrap's PID-namespace lifetime boundary. Open file descriptors retain their existing access. Qwen Code streams host-backed file and named-FIFO stdin through a private read-only socket instead of passing those descriptors to the payload; sockets, character devices and anonymous pipes can be shared. Output standard streams remain the caller's explicit destinations. The execution-status descriptor closes on payload exec.

Landlock also has a narrower writable device surface: it grants writes to `/dev/null`, but does not create bwrap's private `/dev` and `/proc` mounts. Opening `/dev/full` or `/dev/tty` for writing and creating files in `/dev/shm` are denied, so programs requiring these operations may fail. Use the private scratch directory exposed through `TMPDIR`, `TMP`, and `TEMP` for temporary files. Already-open, caller-provided standard streams keep their existing access. Startup's Landlock policy probe requires `/usr/bin/true`; a host missing it fails the probe even if its kernel supports Landlock.

Workspace settings cannot enable, disable or modify this policy, even in a trusted project. Effective precedence is System over User over SystemDefaults, selecting a complete policy object. Values must be literals; environment substitutions, unknown fields and incomplete objects are rejected. `--bare` and `--safe-mode` retain operator confinement. Changes require a new runtime. A malformed or unreadable SystemDefaults, User or System settings file now blocks startup (configuration exit code 52), instead of silently resetting unknown policy to empty settings. The error names the file and repair action. Repair its JSON object or restore read access, then restart. A User `.corrupted` copy is only a reference; the original operator file is never cleared. Workspace-only recovery remains available without operator confinement. Malformed linked Workspace settings (including a linked `.qwen` directory or a hard-linked file) require manual repair; automatic recovery refuses before changing the original or a previous corruption copy. Valid linked JSON remains readable.

This initial public mode supports the ordinary headless CLI and both terminal UIs. ACP, `qwen serve` and web terminals explicitly reject it. MCP/LSP, executable hooks/extensions, worktree/arena management, external agents, speculative execution, file checkpoints, automatic skill/memory maintenance, and unported process or mutation tools are disabled or rejected. Ordinary same-workspace in-process agents cannot add hooks, MCP servers, external executors or worktree isolation. Custom status commands, external Markdown/image renderers, IDE detection and connection, file restore, and host Git diff previews are disabled. Conversation-only rewind remains available.

Explicit operator diagnostics such as `/doctor`, opening or editing files and folders through `/memory`, and changing settings through `/skills` remain trusted host actions. These management commands are not exposed as model-invocable commands. Picking a skill fills the input field; submitting `/skill-name` currently reports an unsupported-mode error before preparing arguments or updating usage records. Skill command execution and the model Skill tool remain unavailable until their preparation writes are confined. Automatic memory and skill maintenance remain disabled in this runtime even if you change their saved settings.

Inspect and verify the active boundary before running a task:

```bash
qwen sandbox
qwen sandbox --verify
qwen sandbox -- sh -c 'printf "confined command\n"'
qwen -y -p "Update the project and run its tests"
```

Linked Git worktrees receive write access to the admitted workspace only. Git common-directory and worktree metadata outside that workspace stay read-only; this mode does not implicitly authorize changing another checkout's repository metadata. Operations requiring those writes can be refused.

The report names the tool boundary, requested/effective backend, enforcement level, Landlock ABI when applicable, workspace, filesystem and command network policy. `--verify` checks workspace writes, denial against a file known to be writable on the host, the expected private or shared PID namespace identity, and the selected network namespace. A passed backend probe establishes admission, not authenticated full-session viability under all inherited environment settings. Run an ordinary task afterwards to test its required tools and services. If verification cannot create its host-side temporary fixture, check `TMPDIR` and directory permissions; this is separate from a boundary refusal. Backend setup errors fail before payload execution and never rerun the command on the host. The one-command subcommand forwards literal arguments and redirected stdin while preserving stdout and stderr byte-for-byte; use terminal `!` for interactive PTY commands.

### Migrating from whole-CLI bwrap

Replace `--sandbox bwrap` and remove `tools.sandbox: "bwrap"`, `QWEN_SANDBOX=bwrap`, `QWEN_SANDBOX_NET` and `QWEN_SANDBOX_PROXY_COMMAND`, then configure `tools.executionSandbox` as above. Restart from a shell outside the old sandbox; inherited `SANDBOX=bwrap` is rejected. `proxied` is not a supported network policy. Do not combine this mode with Docker, Podman, Seatbelt or an inherited whole-CLI sandbox marker. Old bwrap settings produce an explicit migration error.

The Docker, Podman and macOS Seatbelt methods below still run the whole CLI in their existing environment.

## Prerequisites

Before using sandboxing, you need to install and set up Qwen Code:

```bash
npm install @qwen-code/qwen-code
```

To verify the installation

```bash
qwen --version
```

## Overview of sandboxing

Sandboxing isolates potentially dangerous operations (such as shell commands or file modifications) from your host system, providing a security barrier between the CLI and your environment.

The benefits of sandboxing include:

- **Security**: Prevent accidental system damage or data loss.
- **Isolation**: Limit file system access to project directory.
- **Consistency**: Ensure reproducible environments across different systems.
- **Safety**: Reduce risk when working with untrusted code or experimental commands.

> [!note]
>
> **Naming note:** Some sandbox-related environment variables may have used the `GEMINI_*` prefix historically. All new environment variables use the `QWEN_*` prefix.

## Sandboxing methods

Your ideal method of sandboxing may differ depending on your platform and your preferred container solution.

### 1. macOS Seatbelt (macOS only)

Lightweight, built-in sandboxing using `sandbox-exec`.

**Default profile**: `permissive-open` - restricts writes outside the project directory, but allows most other operations and outbound network access.

**Best for**: Fast, no Docker required, strong guardrails for file writes.

### 2. Container-based (Docker/Podman)

Cross-platform sandboxing with complete process isolation.

By default, Qwen Code uses a published sandbox image (configured in the CLI package) and will pull it as needed.

The container sandbox mounts your workspace and your `~/.qwen` directory into the container so auth and settings persist between runs.

**Best for**: Strong isolation on any OS, consistent tooling inside a known image.

### Choosing a method

- **On macOS**:
  - Use Seatbelt when you want lightweight sandboxing (recommended for most users).
  - Use Docker/Podman when you need a full Linux userland (e.g., tools that require Linux binaries).
- **On Linux/Windows**:
  - Use Docker or Podman.

## Quickstart

```bash
# Enable sandboxing with command flag
qwen -s -p "analyze the code structure"

# Or enable sandboxing for your shell session (recommended for CI / scripts)
export QWEN_SANDBOX=true   # true auto-picks a provider (see notes below)
qwen -p "run the test suite"

# Configure in settings.json
{
  "tools": {
    "sandbox": true
  }
}
```

> [!tip]
>
> **Provider selection notes:**
>
> - On **macOS**, `QWEN_SANDBOX=true` typically selects `sandbox-exec` (Seatbelt) if available.
> - On **Linux/Windows**, `QWEN_SANDBOX=true` requires `docker` or `podman` to be installed.
> - To force a provider, set `QWEN_SANDBOX=docker|podman|sandbox-exec`.

## Configuration

### Enable sandboxing (in order of precedence)

1. **Environment variable**: `QWEN_SANDBOX=true|false|docker|podman|sandbox-exec`
2. **Command flag / argument**: `-s`, `--sandbox`, or `--sandbox=<provider>`
3. **Settings file**: `tools.sandbox` in your `settings.json` (e.g., `{"tools": {"sandbox": true}}`).

> [!important]
>
> If `QWEN_SANDBOX` is set, it **overrides** the CLI flag and `settings.json`.

`--sandbox` and `-s` accept an optional value: `true`, `false`, `docker`, `podman` or `sandbox-exec`. For automatic selection with a prompt, use `qwen --sandbox=true "query"` or `qwen --sandbox -p "query"`. Unlike the old boolean-only parser, `qwen --sandbox "query"` now treats `query` as a backend value and rejects an unknown backend. Options before `--` use the same grammar even after positional words; use `-p "..."` for literal option text within a prompt. `--` stops option parsing, but its trailing tokens are not forwarded as the default-command prompt. Session sandbox flags are not accepted on management subcommands, whose `-s` aliases retain their own meaning.

### Configure the sandbox image (Docker/Podman)

- **CLI flag**: `--sandbox-image <image>`
- **Environment variable**: `QWEN_SANDBOX_IMAGE=<image>`
- **Settings file**: `tools.sandboxImage` in your `settings.json` (e.g., `{"tools": {"sandboxImage": "ghcr.io/qwenlm/qwen-code:0.14.1"}}`)

Priority order (highest to lowest):

1. `--sandbox-image`
2. `QWEN_SANDBOX_IMAGE`
3. `tools.sandboxImage`
4. Built-in default image from the CLI package (for example `ghcr.io/qwenlm/qwen-code:<version>`)

`settings.env.QWEN_SANDBOX_IMAGE` also works as a generic env injection mechanism, but `tools.sandboxImage` is the preferred persistent setting.

Custom images are user-managed. Rebuild them with an up-to-date Qwen Code installation to receive the safe update handoff; older images may still use their original in-process updater.

### macOS Seatbelt profiles

Built-in profiles (set via `SEATBELT_PROFILE` env var):

- `permissive-open` (default): Write restrictions, network allowed
- `permissive-closed`: Write restrictions, no network
- `permissive-proxied`: Write restrictions, network via proxy
- `restrictive-open`: Strict restrictions, network allowed
- `restrictive-closed`: Maximum restrictions
- `restrictive-proxied`: Strict restrictions, network via proxy

> [!tip]
>
> Start with `permissive-open`, then tighten to `restrictive-closed` if your workflow still works.

### Custom Seatbelt profiles (macOS)

To use a custom Seatbelt profile:

1. Create a file named `.qwen/sandbox-macos-<profile_name>.sb` in your project.
2. Set `SEATBELT_PROFILE=<profile_name>`.

### Custom Sandbox Flags

For container-based sandboxing, you can inject custom flags into the `docker` or `podman` command using the `SANDBOX_FLAGS` environment variable. This is useful for advanced configurations, such as disabling security features for specific use cases.

**Example (Podman)**:

To disable SELinux labeling for volume mounts, you can set the following:

```bash
export SANDBOX_FLAGS="--security-opt label=disable"
```

Multiple flags can be provided as a space-separated string:

```bash
export SANDBOX_FLAGS="--flag1 --flag2=value"
```

### Network proxying (all sandbox methods)

If you want to restrict outbound network access to an allowlist, you can run a local proxy alongside the sandbox:

- Set `QWEN_SANDBOX_PROXY_COMMAND=<command>`
- The command must start a proxy server that listens on `:::8877`

This is especially useful with `*-proxied` Seatbelt profiles.

For a working allowlist-style proxy example, see: [Example Proxy Script](../../developers/examples/proxy-script.md).

## Linux UID/GID handling

On Linux, Qwen Code defaults to enabling UID/GID mapping so the sandbox runs as your user (and reuses the mounted `~/.qwen`). Override with:

```bash
export SANDBOX_SET_UID_GID=true   # Force host UID/GID
export SANDBOX_SET_UID_GID=false  # Disable UID/GID mapping
```

## Troubleshooting

### Common issues

**"Operation not permitted"**

- Operation requires access outside sandbox.
- On macOS Seatbelt: try a more permissive `SEATBELT_PROFILE`.
- On Docker/Podman: verify the workspace is mounted and your command doesn’t require access outside the project directory.

**Missing commands**

- Container sandbox: add them via `.qwen/sandbox.Dockerfile` or `.qwen/sandbox.bashrc`.
- Seatbelt: your host binaries are used, but the sandbox may restrict access to some paths.

**Java not available in Docker sandbox**

The official Qwen Code Docker image is intentionally minimal to keep the image small, secure, and fast to pull. Different users require different language runtimes (Java, Python, Node.js, etc.), and bundling all environments into a single image is not practical. Therefore, Java is **not included by default** in the Docker sandbox.

If your workflow requires Java, you can extend the base image by creating a `.qwen/sandbox.Dockerfile` in your project:

```dockerfile
FROM ghcr.io/qwenlm/qwen-code:latest

RUN apt-get update && \
    apt-get install -y openjdk-17-jre && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
```

Then rebuild the sandbox image:

```bash
QWEN_SANDBOX=docker BUILD_SANDBOX=1 qwen -s
```

For more details on customizing the sandbox, see [Customizing the sandbox environment](../../developers/tools/sandbox.md).

**Network issues**

- Check sandbox profile allows network.
- Verify proxy configuration.

### Debug mode

```bash
DEBUG=1 qwen -s -p "debug command"
```

**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect the CLI due to automatic exclusion. Use `.qwen/.env` files for Qwen Code-specific debug settings.

### Inspect sandbox

```bash
# Check environment
qwen -s -p "run shell command: env | grep SANDBOX"

# List mounts
qwen -s -p "run shell command: mount | grep workspace"
```

## Security notes

- Sandboxing reduces but doesn't eliminate all risks.
- Use the most restrictive profile that allows your work.
- Container overhead is minimal after the first pull/build.
- GUI applications may not work in sandboxes.

## Related documentation

- [Configuration](../configuration/settings): Full configuration options.
- [Commands](../features/commands): Available commands.
- [Troubleshooting](../support/troubleshooting): General troubleshooting.

---

### Users Features Scheduled Tasks (`docs/users/features/scheduled-tasks.md`)

# Run Prompts on a Schedule

> Use `/loop` and the cron scheduling tools to run prompts repeatedly, poll for status, or set one-time reminders within a Qwen Code session.

Scheduled tasks let Qwen Code re-run a prompt automatically on an interval. Use them to poll a deployment, babysit a PR, check back on a long-running build, or remind yourself to do something later in the session.

Tasks created from the terminal are session-scoped: they live in the current Qwen Code process and are gone when you exit. Nothing is written to disk.

Messaging channels use a separate persistent scheduler so results can be pushed back to the originating chat after the current turn. See [Scheduled Channel Loops](./channels/overview#scheduled-channel-loops) for the channel commands, persistence behavior, and delivery constraints.

> **Tip:** Scheduled tasks are enabled by default. To disable them, set `experimental.cron: false` in your [settings](../configuration/settings.md), or set `QWEN_CODE_DISABLE_CRON=1` in your environment.

## Schedule a recurring prompt with /loop

The `/loop` [bundled skill](skills.md) is the quickest way to schedule a recurring prompt. Pass an optional interval and a prompt, and Qwen Code sets up a cron job that fires in the background while the session stays open.

```text
/loop 5m check if the deployment finished and tell me what happened
```

Qwen Code parses the interval, converts it to a cron expression, schedules the job, and confirms the cadence and job ID. It then immediately executes the prompt once — you don't have to wait for the first cron fire.

### Interval syntax

Intervals are optional. You can lead with them, trail with them, or leave them out entirely.

| Form                    | Example                               | Parsed interval              |
| :---------------------- | :------------------------------------ | :--------------------------- |
| Leading token           | `/loop 30m check the build`           | every 30 minutes             |
| Trailing `every` clause | `/loop check the build every 2 hours` | every 2 hours                |
| No interval             | `/loop check the build`               | defaults to every 10 minutes |

Supported units are `s` for seconds, `m` for minutes, `h` for hours, and `d` for days. Seconds are rounded up to the nearest minute since cron has one-minute granularity. Intervals that don't divide evenly into their unit, such as `7m` or `90m`, are rounded to the nearest clean interval and Qwen Code tells you what it picked.

### Loop over another command

The scheduled prompt can itself be a command or skill invocation. This is useful for re-running a workflow you've already packaged.

```text
/loop 20m /review-pr 1234
```

Each time the job fires, Qwen Code runs `/review-pr 1234` as if you had typed it.

### Autonomous mode

Running `/loop` with **no prompt** starts an autonomous loop instead of repeating a fixed prompt. Qwen Code acts as a steward of the work already established in the conversation — it keeps your work moving while you're away:

```text
/loop
```

A bare `/loop` (no prompt, no interval) runs a self-paced autonomous loop; `/loop <interval>` with no prompt runs the same autonomous loop on a fixed cadence (e.g. `/loop 10m`). On each fire it advances what the conversation already set up — finishing things you started, maintaining an in-progress PR (addressing review threads, fixing failing CI, resolving conflicts), and honoring follow-up commitments. It only acts on work the transcript already established: it never invents new work or makes irreversible changes (push, delete, send) without clear authorization, and it stops once everything is quiet.

### Manage loops

`/loop` also supports two subcommands for managing existing jobs:

```text
/loop list
```

Lists all scheduled jobs with their IDs and cron expressions.

```text
/loop clear
```

Cancels all scheduled jobs at once.

## Set a one-time reminder

For one-shot reminders, describe what you want in natural language instead of using `/loop`. Qwen Code schedules a single-fire task that deletes itself after running.

```text
remind me at 3pm to push the release branch
```

```text
in 45 minutes, check whether the integration tests passed
```

Qwen Code pins the fire time to a specific minute and hour using a cron expression and confirms when it will fire.

## Wake the session only when something changed

A recurring task can carry a **gate**: a shell command that runs at each fire time, outside the model. If it prints nothing and exits 0, the fire is skipped — no turn runs and nothing is added to the conversation. If it prints output, the task's prompt runs with that output attached and marked as data produced by the command. A command that fails, prints binary data or runs longer than 60 seconds is reported once, then counted silently until it succeeds again; `CronList` shows the count.

Use a gate for watches that are usually quiet, with a command that keeps its own cursor and prints only what changed:

```text
every 2 minutes, run ./check-pr.sh 15057 as a gate; when it prints something, handle the events it lists
```

Qwen Code calls `CronCreate` with `gate: "./check-pr.sh 15057"` and confirms the command the way it confirms a `Monitor` command: once, when the task is created, with the option to always allow it. The command then runs unattended from the project directory with the session's environment. A durable gated task keeps working after a restart when the command is read-only, a saved `Monitor(...)` or `Bash(...)` rule allows it, or the session runs in auto or yolo mode; otherwise the next fire reports the rule to add instead of running the command. A deny rule for the command stops the gate in every mode.

Write a gated prompt as instructions ("for each event in the gate output, …"), not as a slash command. A gate is not yet available for tasks bound to the current daemon conversation (`sessionMode: "current"`). In a `qwen serve` daemon, "Run now" on a gated task asks the model to run the gate itself first and to follow the task only when the command exits 0 and prints something.

## Manage scheduled tasks

Ask Qwen Code in natural language to list or cancel tasks, or reference the underlying tools directly.

```text
what scheduled tasks do I have?
```

```text
cancel the deploy check job
```

Under the hood, Qwen Code uses these tools:

| Tool         | Purpose                                                                                                                                   |
| :----------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
| `CronCreate` | Schedule a new task. Accepts a 5-field cron expression, the prompt to run, whether it recurs or fires once, and an optional gate command. |
| `CronList`   | List all scheduled tasks with their IDs, schedules, prompts, and the gate and its recent outcomes when one is set.                        |
| `CronDelete` | Cancel a task by ID.                                                                                                                      |

Each scheduled task has an 8-character ID you can pass to `CronDelete`. A session can hold up to 50 scheduled tasks at once.

## How scheduled tasks run

The scheduler checks every second for due tasks and enqueues them when the session is idle. A scheduled prompt fires between your turns, not while Qwen Code is mid-response. If Qwen Code is busy when a task comes due, the prompt waits until the current turn ends. A gated task runs its gate command first and is enqueued only when the command prints output.

All times are interpreted in your local timezone. A cron expression like `0 9 * * *` means 9am wherever you're running Qwen Code, not UTC.

### Jitter

To avoid every session hitting the API at the same wall-clock moment, the scheduler adds a small deterministic offset to fire times:

- **Recurring tasks** fire up to 10% of their period late, capped at 15 minutes. An hourly job might fire anywhere from `:00` to `:06`.
- **One-shot tasks** scheduled for the top or bottom of the hour (minute `:00` or `:30`) fire up to 90 seconds early.

The offset is derived from the task ID, so the same task always gets the same offset. If exact timing matters, pick a minute that is not `:00` or `:30`, for example `3 9 * * *` instead of `0 9 * * *`, and the one-shot jitter will not apply.

### Recurring-task expiry

Recurring tasks automatically expire 7 days after creation by default. The task fires one final time, then deletes itself. This bounds how long a forgotten loop can run.

To change the limit, set `experimental.cronRecurringMaxAgeDays` in your [settings](../configuration/settings.md), or set the `QWEN_CODE_CRON_MAX_AGE_DAYS` environment variable (the environment variable wins — convenient for cloud or container deployments where editing `settings.json` is impractical). A value of `0` disables expiry entirely, so tasks run until you delete them — useful for long-running daemon deployments that host daily reports, digests, or ongoing monitoring. The configured limit also applies to durable tasks restored from disk after a restart.

One-shot tasks do not expire on a timer — they simply delete themselves after firing once.

## Cron expression reference

`CronCreate` accepts standard 5-field cron expressions: `minute hour day-of-month month day-of-week`. All fields support wildcards (`*`), single values (`5`), steps (`*/15`), ranges (`1-5`), and comma-separated lists (`1,15,30`).

| Example        | Meaning                      |
| :------------- | :--------------------------- |
| `*/5 * * * *`  | Every 5 minutes              |
| `0 * * * *`    | Every hour on the hour       |
| `7 * * * *`    | Every hour at 7 minutes past |
| `0 9 * * *`    | Every day at 9am local       |
| `0 9 * * 1-5`  | Weekdays at 9am local        |
| `30 14 15 3 *` | March 15 at 2:30pm local     |

Day-of-week uses `0` or `7` for Sunday through `6` for Saturday. When both day-of-month and day-of-week are constrained (neither is `*`), a date matches if either field matches — this follows standard vixie-cron semantics.

Extended syntax like `L`, `W`, `?`, and name aliases such as `MON` or `JAN` is not supported.

## Limitations

Session-scoped scheduling has inherent constraints:

- Tasks only fire while Qwen Code is running and idle. Closing the terminal or letting the session exit cancels everything.
- No catch-up for missed fires. If a task's scheduled time passes while Qwen Code is busy on a long-running request, it fires once when Qwen Code becomes idle, not once per missed interval.
- No persistence across restarts. Restarting Qwen Code clears all session-scoped tasks.

---

### Users Features Skills (`docs/users/features/skills.md`)

# Agent Skills

> Create, manage, and share Skills to extend Qwen Code's capabilities.

This guide shows you how to create, use, and manage Agent Skills in **Qwen Code**. Skills are modular capabilities that extend the model's effectiveness through organized folders containing instructions (and optionally scripts/resources).

## Prerequisites

- Qwen Code (recent version)
- Basic familiarity with Qwen Code ([Quickstart](../quickstart.md))

## What are Agent Skills?

Agent Skills package expertise into discoverable capabilities. Each Skill consists of a `SKILL.md` file with instructions that the model can load when relevant, plus optional supporting files like scripts and templates.

### How Skills are invoked

Skills are **model-invoked** — the model autonomously decides when to use them based on your request and the Skill's description. This is different from slash commands, which are **user-invoked** (you explicitly type `/command`).

If you want to invoke a Skill explicitly, type it as a slash command using the Skill's name:

```bash
/<skill-name>
```

Start typing `/` to autocomplete and browse available Skills alongside their descriptions. The `/skills` command opens the Skills panel, where you can browse, search, toggle, and launch Skills interactively.

`<skill-name>` is always the Skill's registered name. For a Skill from an installed extension that name carries its owner — `rust:pdf`, not `pdf` — so you type `/rust:pdf`. See [How extension Skills are named](#how-extension-skills-are-named).

> **Note:** If you previously ran a Skill with `/skills <skill-name>`, that syntax now just opens the Skills panel and ignores the trailing argument. Use `/<skill-name>` to run a Skill directly.

### Benefits

- Extend Qwen Code for your workflows
- Share expertise across your team via git
- Reduce repetitive prompting
- Compose multiple Skills for complex tasks

## Create a Skill

Skills are stored as directories containing a `SKILL.md` file.

### Generate a project Skill with `/learn`

Use `/learn` to distill an existing knowledge source into a reusable project
Skill:

```text
/learn https://docs.example.com/api
/learn ~/projects/acme-sdk
/learn Our deploy process: run migrate, deploy the service, then check health
```

The command runs as a normal agent turn and creates the result under
`.qwen/skills/learned-skill-<name>/SKILL.md` with `source: learned` in its
frontmatter. Review the generated instructions before using or sharing them.

`/learn` also accepts local or direct-link `.mp4`, `.webm`, `.mov`, and `.m4v`
videos. Add text after the path or URL to focus the generated Skill on one part
of the tutorial:

```text
/learn ./tutorial.mp4 focus on the deployment workflow
```

Video learning requires a video-capable model on an OpenAI-compatible provider.
YouTube page URLs are not direct video input; download the video into the
workspace and pass its local path instead.

### Personal Skills

Personal Skills are available across all your projects. Store them in `~/.qwen/skills/`:

```bash
mkdir -p ~/.qwen/skills/my-skill-name
```

Qwen Code also reads personal Skills from `~/.agents/skills/`. If both folders contain a Skill with the same name, the one in `~/.qwen/skills/` wins.

Use personal Skills for:

- Your individual workflows and preferences
- Skills you're developing
- Personal productivity helpers

### Project Skills

Project Skills are shared with your team. Store them in `.qwen/skills/` within your project:

```bash
mkdir -p .qwen/skills/my-skill-name
```

Qwen Code also reads project Skills from `.agents/skills/`. If both folders contain a Skill with the same name, the one in `.qwen/skills/` wins.

Use project Skills for:

- Team workflows and conventions
- Project-specific expertise
- Shared utilities and scripts

Project Skills can be checked into git and automatically become available to teammates.

### Maintain auto-generated project Skills

Qwen Code tracks successful uses of generated project Skills locally, including while new Auto Skill generation is disabled, so re-enabling maintenance cannot mistake a recently used skill for an inactive one. When **Auto Skill** is enabled, it periodically moves inactive generated Skills out of the active library. Only directories named `.qwen/skills/auto-skill-*` whose `SKILL.md` frontmatter contains `source: auto-skill` are managed; personal, extension, bundled, and hand-authored Skills are never selected.

- After 30 days without a successful use or `SKILL.md` edit, an auto-skill is marked stale.
- After 90 days, its complete directory is moved to `.qwen/archived-skills/`. Nothing is permanently deleted.
- Automatic maintenance runs at most once every 7 days in trusted workspaces. Each newly observed auto-skill gets a full grace period before maintenance begins.
- A pinned auto-skill is excluded from automatic stale and archive transitions until it is unpinned.
- Archived directory names remain reserved, and an existing archive destination skips only that collision rather than stopping maintenance for other skills.

Use `/curator` to see active, stale, archived, and pinned auto-skills. Run `/curator run --dry-run` to preview a maintenance pass, `/curator run` to apply it immediately, `/curator pin <directory>` or `/curator unpin <directory>` to control per-skill maintenance, or `/curator restore <directory>` to move an archived auto-skill back into the active library.

Status and dry-run previews are available in safe mode and untrusted workspaces. Applying maintenance, changing pins, and restoring archived auto-skills require a trusted workspace outside safe mode.

## Write `SKILL.md`

Create a `SKILL.md` file with YAML frontmatter and Markdown content:

```yaml
---
name: your-skill-name
description: Brief description of what this Skill does and when to use it
priority: 10
---

# Your Skill Name

## Instructions
Provide clear, step-by-step guidance for Qwen Code.

## Examples
Show concrete examples of using this Skill.
```

### Field requirements

Qwen Code currently validates that:

- `name` is a non-empty string matching `/^[\p{L}\p{N}_:.-]+$/u` — Unicode letters and digits (CJK / Cyrillic / accented Latin all OK), plus `_`, `:`, `.`, `-`. Whitespace, slashes, brackets and other structurally unsafe characters are rejected at parse time. The admitted `:` is what lets a Skill registered by an extension (`rust:pdf`) and an author who chooses a colon themselves (`rust:chat`, written inside the `rust` extension) share one pattern, so a colon in a registered name is not proof of an owner — see [How extension Skills are named](#how-extension-skills-are-named).
- `description` is a non-empty string
- `priority` is optional. When present, it must be a finite number. Higher values sort earlier in the `/skills` listing only — slash-command completion (typing `/`) and the `/help` custom commands view stay alphabetical, so a high-priority Skill never reorders built-in commands. Omitted or invalid values are treated as unset, which behaves like `0`.

Recommended conventions:

- Prefer lowercase ASCII with hyphens for shareable names (e.g. `tsx-helper`)
- Make `description` specific: include both **what** the Skill does and **when** to use it (key words users will naturally mention)
- Use `priority` sparingly for Skills that should reliably appear before the default alphabetical order in `/skills`. Negative priorities are allowed and sort below unset Skills.

### Optional: gate a Skill on file paths (`paths:`)

For Skills that only matter to specific parts of a codebase, add a `paths:` list of glob patterns. The Skill stays out of the model's available-skills listing until a tool call touches a matching file:

```yaml
---
name: tsx-helper
description: React TSX component helper
paths:
  - 'src/**/*.tsx'
  - 'packages/*/src/**/*.tsx'
---
```

Notes:

- Globs are matched relative to the project root with [picomatch](https://github.com/micromatch/picomatch); files outside the project root never trigger activation.
- A path-gated Skill **stays activated for the rest of the session** once a matching file is touched. A new session, or a `refreshCache` triggered by editing any Skill file, resets activations.
- `paths:` only gates **model** discovery, and only at the SkillTool listing level. Unless `user-invocable: false` is set, you can always invoke a path-gated Skill yourself via `/<skill-name>` or the `/skills` picker — that user path runs the Skill body regardless of activation state. The model side, however, stays gated until a matching file is touched: a slash invocation does **not** unlock model-side activation, so if you want the model to chain off your invocation (call `Skill { skill: ... }` itself), also access a file matching the skill's `paths:` first.
- Combining `paths:` with `disable-model-invocation: true` is allowed but the gate has no effect — the Skill is hidden from the model regardless, so path activation never advertises it.

### Optional: control user and model invocation

Skills are user-invocable by default. To hide a Skill from direct slash-command use while keeping it available for model invocation, set `user-invocable: false`:

```yaml
---
name: model-only-helper
description: Helper the model can call when appropriate
user-invocable: false
---
```

This removes the Skill from `/<skill-name>` invocation and `/skills` picker results. It does not hide the Skill from the model.

To hide a Skill from model invocation while keeping direct user invocation available, set `disable-model-invocation: true`:

```yaml
---
name: manual-helper
description: Helper you invoke manually
disable-model-invocation: true
---
```

You can combine both fields, but then the Skill is not reachable through the normal user or model invocation paths.

### Optional: enforce a rule deterministically (`hooks:`)

Everything in a `SKILL.md` body is an instruction to the model: it is prompt text, so following it depends on the model. When a rule must hold no matter what the model decides — refuse to run unless a required value was injected, never touch a protected path — declare a [hook](hooks.md) in the frontmatter instead. Hooks run as code, so they do not depend on the model's cooperation:

```yaml
---
name: gated-skill
description: Calls the downstream CLI using a runtime-injected session ID
hooks:
  PreToolUse:
    - matcher: run_shell_command
      hooks:
        - type: command
          command: '"$QWEN_SKILL_ROOT/scripts/gate-session-id.sh"'
---
```

`$QWEN_SKILL_ROOT` is set to the Skill's own directory, so hook commands can reference files shipped alongside `SKILL.md`. The command string is handed to a shell, so **keep the inner quotes**: unquoted, a project path containing a space splits into two words and the gate never runs. **Make the script executable** (`chmod +x`) too. Both mistakes fail open in the same way: the tool call proceeds, and nothing appears in the transcript or the log to say the gate did not run. A `PreToolUse` hook blocks the tool call when it exits with code `2` (stderr is fed back to the model as the reason), or when it prints `hookSpecificOutput.permissionDecision: "deny"`:

```bash
#!/usr/bin/env bash
if [ -z "${DOWNSTREAM_SESSION_ID:-}" ]; then
  echo "Required input DOWNSTREAM_SESSION_ID is not available. Cannot proceed." >&2
  exit 2
fi
exit 0
```

Notes:

- Hooks are registered when the Skill is invoked and last for the rest of the session. This is true on both invocation paths — whether the model calls the Skill or you type `/<skill-name>`.
- Resuming a session with `--continue` / `--resume` re-applies both halves — the `allowedTools` allow rules and the `hooks:` — of every Skill the **model** loaded through the Skill tool, because those invocations are recorded in the conversation as tool calls. The grants last for the resumed session only, the same as a live invocation. A Skill you started yourself with `/<skill-name>` is submitted as an ordinary prompt and leaves no tool-call record, so it is not restored — re-run `/<skill-name>` after resuming to re-arm its gate.
- Resume does not re-apply them for a Skill a fresh tool call would refuse today (disabled, hidden from the model with `disable-model-invocation`, or a `paths:` Skill not yet activated), for a **project** Skill whose folder is no longer trusted, or when the recorded body does not match the body in `SKILL.md` — because it was edited, or because it was too long and was truncated for the model. In these cases a Skill that declares `hooks:` or `allowedTools` leaves a debug log line saying why. Resume also skips, without a log line, a Skill loaded from inside an `exec` script and a Skill that is no longer discovered (for example, one that was removed or renamed). The match covers the body only, so an edit to just the frontmatter is re-applied with the new `allowedTools` and `hooks:`.
- Registration is idempotent: re-invoking a Skill does not stack duplicate hooks.
- A Skill hook's `matcher:` follows the same rules as a settings hook (see [Hooks](hooks.md)). An omitted or empty `matcher:` matches every tool, and matchers are not anchored, so `edit` also matches `notebook_edit`. Skill hooks used to wrap their matcher in `^…$` and to match nothing when it was omitted: write `^edit$` to match exactly one tool, and write exclusions as `^(?!write_file).*$`.
- The `command:` runs through the platform shell: `bash` on macOS and Linux, and on Windows Git Bash when it is detected (`MSYSTEM`/`TERM`), otherwise `cmd.exe` or PowerShell. The example above is POSIX shell — under `cmd.exe` `$QWEN_SKILL_ROOT` is not expanded and a `.sh` script is not executable, so the gate fails open there. A hook may set `shell: bash` to force bash, but that resolves to whatever `bash` is on `PATH`, so on Windows outside Git Bash write the gate for the shell you actually have.
- Sessions that disable hooks register none of them — `disableAllHooks`, safe mode, and an ACP client's `skipHooks`. The Skill's body and its `allowedTools` still apply in those sessions, but its gate does not, so a rule you rely on a hook to enforce is not enforced there. Bare mode goes further: no Skills are discovered at all, so there is no body and no `allowedTools` either.
- A **project** Skill's hooks run repo-supplied commands, so they are registered only in a trusted folder, and trust is re-read every time a hook fires and every time a permission is decided. With an IDE companion connected that value is live: revoking trust silences an already-registered gate — and suspends the Skill's `allowedTools` — at the next tool call, without a restart. Without an IDE connection the value is fixed when the CLI starts, so a change made through the CLI's own trust dialog takes effect on restart. Granting trust never retro-registers: invoke the Skill again.
- `hooks:` is read for project, user, and bundled Skills. Extension-provided Skills do not support it; use the extension's own manifest-level hooks instead.
- See [Hooks](hooks.md) for the full event list, matcher syntax, and output format.

## Add supporting files

Create additional files alongside `SKILL.md`:

```text
my-skill/
├── SKILL.md (required)
├── reference.md (optional documentation)
├── examples.md (optional examples)
├── scripts/
│   └── helper.py (optional utility)
└── templates/
    └── template.txt (optional template)
```

Reference these files from `SKILL.md`:

````markdown
For advanced usage, see [reference.md](reference.md).

Run the helper script:

```bash
python scripts/helper.py input.txt
```
````

## View available Skills

Qwen Code discovers Skills from:

- Personal Skills: `~/.qwen/skills/` and `~/.agents/skills/`
- Project Skills: `.qwen/skills/` and `.agents/skills/`
- Custom directories: paths listed in [`skills.directories`](../configuration/settings#skills), loaded as personal Skills
- Extension Skills: Skills provided by installed extensions
- Bundled Skills: Skills shipped with Qwen Code

When two Skills share a name, the first one found wins: project, then personal (including custom directories), then extension, then bundled.

### Share Skills with other tools

If you also use another agent, you can keep a single skills folder instead of copying Skills into `~/.qwen/skills/`:

- Add the other agent's folder to `skills.directories`, for example `["~/.claude/skills"]`. Use an absolute or `~`-prefixed path: a relative path resolves against the working directory, not the project root. These Skills load as personal Skills, so their `allowedTools` and hooks are not gated by folder trust, even when a trusted project's settings add the directory: only point this at folders you control.
- Or keep shared Skills in `~/.agents/skills/` or `.agents/skills/`, which Qwen Code reads without any configuration. Symlinked Skill folders are followed, and their target can live anywhere on disk.

Qwen Code reads the `allowedTools` frontmatter field. An `allowed-tools` field written for another agent is ignored.

### Extension Skills

Extensions can provide custom skills that become available when the extension is enabled. These skills are stored in the extension's `skills/` directory and follow the same format as personal and project skills.

Extension skills are automatically discovered and loaded when the extension is installed and enabled.

To see which extensions provide skills, check the extension's `qwen-extension.json` file for a `skills` field.

#### How extension Skills are named

Qwen Code registers a Skill from an installed extension as `<extensionName>:<name>`, where `<extensionName>` is the `name` field of that extension's `qwen-extension.json` and `<name>` is the Skill's own frontmatter `name`. A Skill named `pdf` in the `rust` extension is registered as `rust:pdf`.

The prefix is added while the Skill is loaded, not written into the file: your `SKILL.md` keeps the name you authored, and Qwen Code never recovers the authored name by splitting the registered one apart (an author may legitimately write `rust:chat` inside `rust`). Only extension Skills are prefixed — personal, project, and bundled Skills keep the single spelling you authored.

Use the registered name everywhere you refer to the Skill:

- Invoke it as `/rust:pdf`. The bare `/pdf` is not an alias — the extension's Skill is reachable only under its registered name.
- The model calls it as `Skill { skill: "rust:pdf" }`, the same name it reads in `<available_skills>`.
- Two extensions that each ship a Skill named `pdf` give you two Skills (`rust:pdf` and `docs-suite:pdf`) rather than one winning and one disappearing.

The surfaces where you read and choose Skills also name the owner: the Skills panel (including the rows a setting has locked), the read-only listing a bare `/skills` prints outside the interactive UI (ACP and other non-interactive modes — interactively the command opens the panel), and the badge in the `/` command palette, which reads `[Extension: Rust]` rather than a bare `[Extension]`. Those labels prefer the extension's `displayName` and fall back to its `name` when it declares none.

#### Extension Skills and the `skills.*` settings

`skills.disabled`, `skills.defaultDisabled`, and `slashCommands.disabled` match an extension Skill under **either** spelling, so a `skills.disabled: ["pdf"]` you wrote before the prefix existed still hides `rust:pdf`. A restriction can only remove capability, so renaming a Skill is not allowed to lift one.

`skills.enabled` is the exception, and the one visible change for an existing settings file: it grants capability, so it matches the registered name only. `skills.enabled: ["pdf"]` no longer opts an extension's `pdf` in on its own — write `skills.enabled: ["rust:pdf"]`. The one bare pair that keeps working is a pre-prefix opt-in sitting in `skills.defaultDisabled` with the same spelling: cancellation compares the entries themselves, so `defaultDisabled: ["pdf"]` + `enabled: ["pdf"]` cancels the entry — the skill then ends up enabled per the enablement stored for this workspace, else the extension's own default; for a default-off skill, write `rust:pdf` in `skills.enabled`.

Toggling a Skill in the Skills panel writes the registered name and removes only that entry, so enabling `rust:pdf` leaves a legacy `disabled: ["pdf"]` untouched. When that legacy entry sits in a higher scope — system defaults, user, or system settings — the panel says so and locks the row, naming the scope to edit rather than offering a toggle that cannot move it. A legacy entry in this workspace's own settings locks the row the same way, naming the entry and its scope (`skills.disabled 'pdf' (Workspace)` or `skills.defaultDisabled 'pdf' (Workspace)`) so you know which list in which file to edit.

Two limits worth knowing:

- Cross-level precedence is unchanged and still compares registered names exactly (`project` > `user` > `extension` > `bundled`), so a personal or project Skill you author as `rust:pdf` outranks the extension's `pdf`. Bare-name collisions between a personal or project Skill and a bundled Skill are likewise still settled by that precedence, not by the prefix. A Skill that collides with a custom command is not — on the slash surface the last loader wins, and custom commands load after Skills, so `/pdf` runs the custom command while the Skill stays available to the model.
- Skill names are also used as filenames: the file a Skill reads its invocation arguments from replaces every character outside `[A-Za-z0-9._-]` with `_`, so an extension Skill registered as `rust:pdf` and a personal or project Skill authored `rust_pdf` both resolve to `qwen-skill-args-rust_pdf.txt` and share one arguments file. (The prefix rarely collides with itself — `rust:rust_pdf` becomes `rust_rust_pdf` — but extension names may contain `_`, so `rust_pdf:x` and `rust:pdf_x` fold to the same filename.) Non-ASCII letters fold the same way, so an authored `café` and an authored `caf_` land on `caf_` too — a limitation that predates the prefix, which only makes it easier to hit. Avoid a Skill name that is another name with `:` turned into `_`.

To view available Skills, ask Qwen Code directly:

```text
What Skills are available?
```

> **Heads up — model vs. user view.** Asking the model only surfaces Skills the model can currently see. If a Skill uses `paths:` (see "Optional: gate a Skill on file paths" above), it stays out of that listing until a matching file has been touched. The `/skills` slash command shows Skills you can invoke directly; Skills with `user-invocable: false` remain visible on disk and may still be visible to the model.

Or browse the user-invocable list with the slash command (including path-gated Skills that have not activated yet):

```text
/skills
```

Or inspect the filesystem:

```bash
# List personal Skills
ls ~/.qwen/skills/ ~/.agents/skills/

# List project Skills (if in a project directory)
ls .qwen/skills/ .agents/skills/

# View a specific Skill's content
cat ~/.qwen/skills/my-skill/SKILL.md
```

## Test a Skill

After creating a Skill, test it by asking questions that match your description.

Example: if your description mentions "PDF files":

```text
Can you help me extract text from this PDF?
```

The model autonomously decides to use your Skill if it matches the request — you don't need to explicitly invoke it.

## Debug a Skill

If Qwen Code doesn't use your Skill, check these common issues:

### Make the description specific

Too vague:

```yaml
description: Helps with documents
```

Specific:

```yaml
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDFs, forms, or document extraction.
```

### Verify file path

- Personal Skills: `~/.qwen/skills/<skill-name>/SKILL.md` or `~/.agents/skills/<skill-name>/SKILL.md`
- Project Skills: `.qwen/skills/<skill-name>/SKILL.md` or `.agents/skills/<skill-name>/SKILL.md`
- Custom directories: `<directory>/<skill-name>/SKILL.md` for each entry in `skills.directories`. A Skill nested deeper is not found.

```bash
# Personal
ls ~/.qwen/skills/my-skill/SKILL.md ~/.agents/skills/my-skill/SKILL.md

# Project
ls .qwen/skills/my-skill/SKILL.md .agents/skills/my-skill/SKILL.md

# Custom directory listed in skills.directories
ls ~/shared-skills/my-skill/SKILL.md
```

### Check YAML syntax

Invalid YAML prevents the Skill metadata from loading correctly.

```bash
cat SKILL.md | head -n 15
```

Ensure:

- Opening `---` on line 1
- Closing `---` before Markdown content
- Valid YAML syntax (no tabs, correct indentation)

### View errors

Run Qwen Code with debug mode to see Skill loading errors:

```bash
qwen --debug
```

## Share Skills with your team

You can share Skills through project repositories:

1. Add the Skill under `.qwen/skills/`
2. Commit and push
3. Teammates pull the changes

```bash
git add .qwen/skills/
git commit -m "Add team Skill for PDF processing"
git push
```

## Update a Skill

Edit `SKILL.md` directly:

```bash
# Personal Skill
code ~/.qwen/skills/my-skill/SKILL.md

# Project Skill
code .qwen/skills/my-skill/SKILL.md
```

During a normal session, Qwen Code watches personal and project Skill
directories. Adding, editing, or removing a Skill refreshes the Skill list and
invocation state automatically after a short delay. Bare mode does not start
these watchers, so restart Qwen Code to load Skill changes in that mode.

## Remove a Skill

Delete the Skill directory:

```bash
# Personal
rm -rf ~/.qwen/skills/my-skill

# Project
rm -rf .qwen/skills/my-skill
git commit -m "Remove unused Skill"
```

## Best practices

### Keep Skills focused

One Skill should address one capability:

- Focused: "PDF form filling", "Excel analysis", "Git commit messages"
- Too broad: "Document processing" (split into smaller Skills)

### Write clear descriptions

Help the model discover when to use Skills by including specific triggers:

```yaml
description: Analyze Excel spreadsheets, create pivot tables, and generate charts. Use when working with Excel files, spreadsheets, or .xlsx data.
```

### Test with your team

- Does the Skill activate when expected?
- Are the instructions clear?
- Are there missing examples or edge cases?

---

### Users Features Status Line (`docs/users/features/status-line.md`)

# Status Line

> Display custom information in the footer.

The status line shows session-aware information — model name, token usage, git branch, and more — in the footer's left section. There are two configuration modes:

- **Preset mode** — pick from built-in data items via an interactive dialog or JSON config. No scripting required.
- **Command mode** — run a shell command that receives structured JSON context via stdin. Full flexibility for custom formatting.

```
Single-line status (default approval mode — 1 row):
┌─────────────────────────────────────────────────────────────────┐
│  user@host ~/project (main) ctx:34%   docker | Debug | 67%     │  ← status line
└─────────────────────────────────────────────────────────────────┘

Multi-line status (up to 2 lines — 2 rows):
┌─────────────────────────────────────────────────────────────────┐
│  user@host ~/project (main) ctx:34%   docker | Debug | 67%     │  ← status line 1
│  ████████░░░░░░░░░░ 34% context                                │  ← status line 2
└─────────────────────────────────────────────────────────────────┘

Multi-line status + non-default mode (3 rows max):
┌─────────────────────────────────────────────────────────────────┐
│  user@host ~/project (main) ctx:34%   docker | Debug | 67%     │  ← status line 1
│  ████████░░░░░░░░░░ 34% context                                │  ← status line 2
│  auto-accept edits (shift + tab to cycle)                       │  ← mode indicator
└─────────────────────────────────────────────────────────────────┘
```

When configured, the status line replaces the default "? for shortcuts" hint. High-priority messages (Ctrl+C/D exit prompts, Esc, vim INSERT mode) temporarily override the status line. The status line text is truncated to fit within the available width.

## Quick setup

The easiest way to configure a status line is the `/statusline` command. It opens an interactive dialog where you can select preset items, toggle theme colors, and see a live preview:

```
/statusline
```

This opens the preset mode configurator. Use arrow keys to navigate, space to toggle items, and enter to confirm. Your selection is saved to settings automatically.

When fewer than 16 rows are available, the dialog switches to a compact list-only layout without search or preview.

You can also give `/statusline` specific instructions to have it generate a command-mode configuration:

```
/statusline show model name and context usage percentage
```

---

## Preset mode

Preset mode provides a set of built-in data items that you can pick and combine — no shell commands, no `jq`, no scripting. Items are rendered as `item1 | item2 | item3` in a single line.

### Configuration

Add a `statusLine` object under the `ui` key in `~/.qwen/settings.json`:

```json
{
  "ui": {
    "statusLine": {
      "type": "preset",
      "items": [
        "model-with-reasoning",
        "git-branch",
        "context-remaining",
        "current-dir",
        "context-used"
      ],
      "useThemeColors": true
    }
  }
}
```

| Field                  | Type       | Required | Description                                                                                                                                                                                                                                  |
| ---------------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                 | `"preset"` | Yes      | Must be `"preset"`                                                                                                                                                                                                                           |
| `items`                | string[]   | Yes      | Ordered list of preset item IDs to display (see table below). Items are joined with `\|` as the separator.                                                                                                                                   |
| `useThemeColors`       | boolean    | No       | Apply the active `/theme` color to the status line text. Defaults to `true`.                                                                                                                                                                 |
| `hideContextIndicator` | boolean    | No       | Hide the built-in context usage indicator in the footer right section. When unset, it is hidden automatically if `items` contains `context-used` or `context-remaining`, so context usage is not shown twice. Set `false` to always show it. |

### Available preset items

| Item ID                | Default | Description                                                        |
| ---------------------- | ------- | ------------------------------------------------------------------ |
| `model-with-reasoning` | Yes     | Current model name with reasoning level (e.g. `qwen-3-235b high`)  |
| `model`                |         | Current model name without reasoning level                         |
| `git-branch`           | Yes     | Current Git branch name (hidden when not in a git repo)            |
| `context-remaining`    |         | Percentage of context window remaining (e.g. `Context 65.7% left`) |
| `total-input-tokens`   |         | Cumulative input tokens used in session (e.g. `30.0k total in`)    |
| `total-output-tokens`  |         | Cumulative output tokens used in session (e.g. `5.0k total out`)   |
| `current-dir`          |         | Current working directory                                          |
| `project-name`         | Yes     | Project name (basename of working directory)                       |
| `pull-request-number`  |         | Open PR number for the current branch (requires `gh` CLI)          |
| `branch-changes`       |         | Session file change stats (e.g. `+120 -30`)                        |
| `context-used`         | Yes     | Percentage of context window used (e.g. `Context 34.3% used`)      |
| `run-state`            |         | Compact session state (`Ready`, `Working`, or `Confirm`)           |
| `qwen-version`         |         | Qwen Code version (e.g. `v0.14.1`)                                 |
| `context-window-size`  |         | Total context window size (e.g. `131.1k window`)                   |
| `used-tokens`          |         | Current prompt token count (e.g. `45.0k used`)                     |
| `session-id`           |         | Current session identifier                                         |

Items marked **Default** are pre-selected when you first open the `/statusline` dialog.

`total-input-tokens` and `total-output-tokens` are session totals. They add up token usage across turns, so input tokens can grow quickly because each new model request includes the current conversation context again. Use `used-tokens` when you want the current prompt size instead of cumulative session spend.

### Example output

With the default items, the status line looks like:

```
qwen-3-235b high | main | Context 65.7% left | /home/user/project | Context 34.3% used
```

### Customizing via the dialog

Running `/statusline` opens an interactive multi-select dialog:

```
┌ Configure Status Line ────────────────────────────────────────┐
│ Select which items to display in the status line.             │
│                                                               │
│ Type to search                                                │
│ >                                                             │
│                                                               │
│ [x] Use theme colors        Apply colors from the active /theme│
│ ───────────────────────                                       │
│ [x] model-with-reasoning    Current model name with reasoning │
│ [ ] model-only              Current model name without reason │
│ [x] git-branch              Current Git branch when available │
│ [x] context-remaining       Percentage of context remaining   │
│ ...                                                           │
│                                                               │
│ Preview                                                       │
│ qwen-3-235b high | main | Context 65.7% left                 │
│                                                               │
│ Use up/down to navigate, space to select, enter to confirm    │
└───────────────────────────────────────────────────────────────┘
```

- Type to filter items by name or description
- A live preview updates as you toggle items
- Press enter to save the configuration

---

## Command mode

Command mode runs a shell command whose stdout is displayed in the status line. The command receives structured JSON context via stdin for session-aware output.

### Prerequisites

- [`jq`](https://jqlang.github.io/jq/) is recommended for parsing the JSON input (install via `brew install jq`, `apt install jq`, etc.)
- Simple commands that don't need JSON data (e.g. `git branch --show-current`) work without `jq`

### Configuration

Add a `statusLine` object under the `ui` key in `~/.qwen/settings.json`:

```json
{
  "ui": {
    "statusLine": {
      "type": "command",
      "command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); pct=$(echo \"$input\" | jq -r '.context_window.used_percentage'); echo \"$model  ctx:${pct}%\""
    }
  }
}
```

| Field                  | Type        | Required | Description                                                                                                                                                                                                              |
| ---------------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type`                 | `"command"` | Yes      | Must be `"command"`                                                                                                                                                                                                      |
| `command`              | string      | Yes      | Shell command to execute. Receives JSON via stdin, stdout is displayed (up to 2 lines).                                                                                                                                  |
| `refreshInterval`      | number      | No       | Re-run the command every N seconds (minimum 1). Useful for data that changes without an Agent state event (clock, quota, uptime).                                                                                        |
| `respectUserColors`    | boolean     | No       | Preserve ANSI color codes in command output instead of applying dimmed footer styling. Defaults to `false`.                                                                                                              |
| `hideContextIndicator` | boolean     | No       | Hide the built-in context usage indicator in the footer right section. Defaults to `false` — command output is not inspected for context information, so set it explicitly if your command already prints context usage. |

### JSON input

The command receives a JSON object via stdin with the following fields:

```json
{
  "session_id": "abc-123",
  "version": "0.14.1",
  "model": {
    "display_name": "qwen-3-235b"
  },
  "context_window": {
    "context_window_size": 131072,
    "used_percentage": 34.3,
    "remaining_percentage": 65.7,
    "current_usage": 45000,
    "total_input_tokens": 30000,
    "total_output_tokens": 5000
  },
  "workspace": {
    "current_dir": "/home/user/project"
  },
  "git": {
    "branch": "main"
  },
  "worktree": {
    "name": "fix-auth",
    "path": "/home/user/project/.qwen/worktrees/fix-auth",
    "branch": "fix-auth",
    "original_cwd": "/home/user/project",
    "original_branch": "main"
  },
  "metrics": {
    "models": {
      "qwen-3-235b": {
        "api": {
          "total_requests": 10,
          "total_errors": 0,
          "total_latency_ms": 5000
        },
        "tokens": {
          "prompt": 30000,
          "completion": 5000,
          "total": 35000,
          "cached": 10000,
          "thoughts": 2000
        }
      }
    },
    "files": {
      "total_lines_added": 120,
      "total_lines_removed": 30
    }
  },
  "vim": {
    "mode": "INSERT"
  }
}
```

| Field                                 | Type             | Description                                                                        |
| ------------------------------------- | ---------------- | ---------------------------------------------------------------------------------- |
| `session_id`                          | string           | Unique session identifier                                                          |
| `version`                             | string           | Qwen Code version                                                                  |
| `model.display_name`                  | string           | Current model name                                                                 |
| `context_window.context_window_size`  | number           | Total context window size in tokens                                                |
| `context_window.used_percentage`      | number           | Context window usage as percentage (0–100)                                         |
| `context_window.remaining_percentage` | number           | Context window remaining as percentage (0–100)                                     |
| `context_window.current_usage`        | number           | Token count from the last API call (current context size)                          |
| `context_window.total_input_tokens`   | number           | Total input tokens consumed this session                                           |
| `context_window.total_output_tokens`  | number           | Total output tokens consumed this session                                          |
| `workspace.current_dir`               | string           | Current working directory                                                          |
| `git`                                 | object \| absent | Present only inside a git repository.                                              |
| `git.branch`                          | string           | Current branch name                                                                |
| `worktree`                            | object \| absent | Present only when inside an active worktree (created by `enter_worktree`).         |
| `worktree.name`                       | string           | Worktree slug name                                                                 |
| `worktree.path`                       | string           | Absolute path to the worktree directory                                            |
| `worktree.branch`                     | string           | Branch checked out in the worktree                                                 |
| `worktree.original_cwd`               | string           | Working directory before entering the worktree                                     |
| `worktree.original_branch`            | string           | Branch that was active before entering the worktree                                |
| `metrics.models.<id>.api`             | object           | Per-model API stats: `total_requests`, `total_errors`, `total_latency_ms`          |
| `metrics.models.<id>.tokens`          | object           | Per-model token usage: `prompt`, `completion`, `total`, `cached`, `thoughts`       |
| `metrics.files`                       | object           | File change stats: `total_lines_added`, `total_lines_removed`                      |
| `vim`                                 | object \| absent | Present only when vim mode is enabled. Contains `mode` (`"INSERT"` or `"NORMAL"`). |

> **Important:** stdin can only be read once. Always store it in a variable first: `input=$(cat)`.

### Examples

#### Model and token usage

```json
{
  "ui": {
    "statusLine": {
      "type": "command",
      "command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); pct=$(echo \"$input\" | jq -r '.context_window.used_percentage'); echo \"$model  ctx:${pct}%\""
    }
  }
}
```

Output: `qwen-3-235b  ctx:34%`

#### Git branch + directory

```json
{
  "ui": {
    "statusLine": {
      "type": "command",
      "command": "input=$(cat); branch=$(echo \"$input\" | jq -r '.git.branch // empty'); dir=$(basename \"$(echo \"$input\" | jq -r '.workspace.current_dir')\"); echo \"$dir${branch:+ ($branch)}\""
    }
  }
}
```

Output: `my-project (main)`

> Note: The `git.branch` field is provided directly in the JSON input — no need to shell out to `git`.

#### File change stats

```json
{
  "ui": {
    "statusLine": {
      "type": "command",
      "command": "input=$(cat); added=$(echo \"$input\" | jq -r '.metrics.files.total_lines_added'); removed=$(echo \"$input\" | jq -r '.metrics.files.total_lines_removed'); echo \"+$added/-$removed lines\""
    }
  }
}
```

Output: `+120/-30 lines`

#### Live clock and git branch

Use `refreshInterval` when the statusline shows data that changes without an Agent event (e.g. the clock, uptime, or rate-limit counters):

```json
{
  "ui": {
    "statusLine": {
      "type": "command",
      "command": "input=$(cat); branch=$(echo \"$input\" | jq -r '.git.branch // \"no-git\"'); echo \"$(date +%H:%M:%S)  ($branch)\"",
      "refreshInterval": 1
    }
  }
}
```

Output (refreshed every second): `14:32:07  (main)`

#### Script file for complex commands

For longer commands, save a script file at `~/.qwen/statusline-command.sh`:

```bash
#!/bin/bash
input=$(cat)
model=$(echo "$input" | jq -r '.model.display_name')
pct=$(echo "$input" | jq -r '.context_window.used_percentage')
branch=$(echo "$input" | jq -r '.git.branch // empty')
added=$(echo "$input" | jq -r '.metrics.files.total_lines_added')
removed=$(echo "$input" | jq -r '.metrics.files.total_lines_removed')

parts=()
[ -n "$model" ] && parts+=("$model")
[ -n "$branch" ] && parts+=("($branch)")
[ "$pct" != "0" ] 2>/dev/null && parts+=("ctx:${pct}%")
([ "$added" -gt 0 ] || [ "$removed" -gt 0 ]) 2>/dev/null && parts+=("+${added}/-${removed}")

echo "${parts[*]}"
```

Then reference it in settings:

```json
{
  "ui": {
    "statusLine": {
      "type": "command",
      "command": "bash ~/.qwen/statusline-command.sh"
    }
  }
}
```

## Behavior

**Both modes:**

- **Update triggers**: The status line updates when the model changes, a new message is sent (token count changes), vim mode is toggled, git branch changes, tool calls complete, or file changes occur. Updates are debounced (300ms).
- **Output**: Up to 2 lines. Each line is rendered as a separate row in the footer's left section. Lines that exceed the available width are truncated.
- **Hot reload**: Changes to `ui.statusLine` in settings take effect immediately — no restart required.
- **Removal**: Delete the `ui.statusLine` key from settings to disable. The "? for shortcuts" hint returns.

**Command mode only:**

- **Timeout**: Commands that take longer than 5 seconds are killed. The status line clears on failure.
- **Refresh**: Set `refreshInterval` (seconds) to additionally re-run the command on a timer — useful for data that changes without an Agent event (clock, rate limits, build status).
- **Shell**: Commands run via `/bin/sh` on macOS/Linux. On Windows, `cmd.exe` is used by default — wrap POSIX commands with `bash -c "..."` or point to a bash script (e.g. `bash ~/.qwen/statusline-command.sh`).

**Preset mode only:**

- **No external dependencies**: Preset items are computed internally — no shell commands, no `jq`, no timeouts.
- **Theme integration**: When `useThemeColors` is `true` (default), the status line text uses the active `/theme` color. When `false`, dimmed footer styling is applied.
- **PR lookup**: The `pull-request-number` item runs `gh pr view` in the background (2s timeout). It only triggers when the branch changes, not on every update.

## Troubleshooting

| Problem                     | Cause                          | Fix                                                                                                                                                                                                                                                                                                                                                                                                    |
| --------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Status line not showing     | Config at wrong path           | Must be under `ui.statusLine`, not root-level `statusLine`                                                                                                                                                                                                                                                                                                                                             |
| Empty output (command mode) | Command fails silently         | Test manually: `echo '{"session_id":"test","version":"0.14.1","model":{"display_name":"test"},"context_window":{"context_window_size":0,"used_percentage":0,"remaining_percentage":100,"current_usage":0,"total_input_tokens":0,"total_output_tokens":0},"workspace":{"current_dir":"/tmp"},"metrics":{"models":{},"files":{"total_lines_added":0,"total_lines_removed":0}}}' \| sh -c 'your_command'` |
| Stale data (command mode)   | No trigger fired               | Send a message or switch models to trigger an update — or set `refreshInterval` to re-run the command on a timer                                                                                                                                                                                                                                                                                       |
| Command too slow            | Complex script                 | Optimize the script or move heavy work to a background cache                                                                                                                                                                                                                                                                                                                                           |
| Preset items missing        | Conditional items have no data | `git-branch` is hidden outside git repos; `context-used` is hidden when usage is 0; `branch-changes` is hidden when no files changed. This is expected — items appear once their data is available                                                                                                                                                                                                     |
| PR number not showing       | `gh` CLI not installed         | Install [GitHub CLI](https://cli.github.com/) and authenticate with `gh auth login`. The lookup runs with a 2s timeout                                                                                                                                                                                                                                                                                 |

---

### Users Features Sub Agents (`docs/users/features/sub-agents.md`)

# Subagents

Subagents are specialized AI assistants that handle specific types of tasks within Qwen Code. They allow you to delegate focused work to AI agents that are configured with task-specific prompts, tools, and behaviors.

## What are Subagents?

Subagents are independent AI assistants that:

- **Specialize in specific tasks** - Each Subagent is configured with a focused system prompt for particular types of work
- **Have separate context** - They maintain their own conversation history, separate from your main chat
- **Use controlled tools** - You can configure which tools each Subagent has access to
- **Work autonomously** - Once given a task, they work independently until completion or failure
- **Provide detailed feedback** - You can see their progress, tool usage, and execution statistics in real-time

## Claude Code and Codex subagents

The built-in `claude-code` and `codex` agents delegate to separately installed native tools. Install and authenticate Claude Code with its `claude-agent-acp` adapter, or Codex with its `codex` executable, and make the executable available on `PATH`. These agents use their native model and authentication settings. Qwen Code does not fall back to its own model when the executable is missing.

Both agents default to foreground execution; set `run_in_background: true` to receive a background completion notification. They require a trusted workspace and are unavailable in safe mode. Both executors support macOS/Linux (including WSL); native Windows launches are rejected before startup with platform guidance.

Claude Code uses the ACP executor and supports continued input while its session is retained. Codex uses an ephemeral app-server thread for a single task and returns the final answer. Codex tasks cannot receive messages or resume; start a new task instead. Native tool progress, token counts, and cost are not reported for Codex. Native sessions cannot be restored after restarting Qwen Code.

For a custom Codex agent, use the existing `executor` frontmatter:

```markdown
---
name: codex-review
description: Review code with Codex
executor:
  kind: codex
  command: codex
background: false
---

Review the changes and report verified defects.
```

Omitting `executor.args` starts `codex app-server --stdio`; supplied arguments replace that default. Use `kind: acp` and `command: claude-agent-acp` for a custom Claude Code agent. Qwen model overrides, tool lists, subagent hooks, `maxTurns`, fork history, teams, and workflows are not supported for external executors. Worktree launches use the existing Agent isolation lifecycle and run the native process in the selected worktree.

Codex runs unattended. Without an agent override, default, plan, and auto sessions use a read-only sandbox; Qwen's AUTO classifier does not inspect native commands. Intermediate Qwen subagent modes do not grant native access during nested delegation. Explicitly select auto-edit in the session or Codex agent definition to allow workspace writes and unattended workspace commands, or yolo for full access. A session already in auto-edit or yolo takes precedence over a stricter agent definition. Other effective approval modes are rejected. Native requests for extra permission or user input are declined. A configured `runConfig.max_time_minutes` bounds execution. The executor waits for process cleanup on cancellation; the shared background cancellation notification can arrive earlier under its five-second fallback.

## Fork Subagent

In addition to named subagents, Qwen Code supports **forking** — selected explicitly with `subagent_type: "fork"`. A fork inherits the parent's full conversation context and normally runs detached in the background. Forks work in both interactive and headless sessions; headless forks always use the background path. Omitting `subagent_type` does **not** fork; it launches the general-purpose subagent. Top-level named subagents run in the background by default and deliver their results through completion notifications. Set `run_in_background: false` when the current turn must wait for a regular subagent's result inline.

## Fork Context with `fork_turns`

Only `subagent_type: "fork"` accepts `fork_turns`:

- Omitting it or using `all` inherits the full parent conversation.
- A positive integer string such as `"3"` inherits the most recent three real user turns.

Tool responses and pure system reminders do not count as user turns. Regular named subagents and agent-team teammates do not accept `fork_turns`; they keep their separate conversation context.

## Restricting Fork Tool Execution with `fork_tools`

Only `subagent_type: "fork"` accepts `fork_tools`. The array may contain exact canonical tool names, such as `read_file` and `grep_search`, or MCP server patterns such as `mcp__github`. The fork still receives the same model-visible tool declarations as an unrestricted fork, preserving its prompt-cache prefix, but its task prompt identifies the restriction and a call not matched by `fork_tools` is rejected before scheduling or approval.

- Forks never execute `ask_user_question`; when user input is required, they report the blocker to their parent agent.
- Omitting `fork_tools` allows every other inherited tool.
- An empty array rejects every tool call.
- `*` is not accepted; omit `fork_tools` to allow every otherwise-executable inherited tool.
- Tool names cannot have surrounding whitespace. Wildcards are accepted only as `mcp__*` or as a trailing MCP tool-prefix pattern such as `mcp__github__read_*`.
- `mcp__*` intentionally allows every MCP tool while still denying unlisted built-in tools.
- Shell command argument patterns are not supported. Listing `run_shell_command` allows that tool to proceed through its normal permission checks but does not pre-approve any command.

This is a per-invocation restriction supplied by the caller. It narrows a child fork's capabilities but is not an administrator-enforced security sandbox because the caller can omit or expand the list.

## Reusing Fork Restrictions with `fork_profile`

A project can save a named fork restriction in `.qwen/fork-profiles/<name>.md` and select it with `fork_profile`. This is useful when several calls need the same tool boundary and task guidance:

```markdown
---
name: ro-research
tools:
  - read_file
  - grep_search
  - glob
  - mcp__search__*
promptHint: |
  Work read-only. Prefer targeted searches and cite file evidence.
---
```

Then launch the fork with:

```text
agent(description="Research", prompt="Inspect the retry path", subagent_type="fork", fork_profile="ro-research")
```

- `fork_profile` is valid only for a fork and cannot be combined with `fork_tools` or a named teammate.
- Profiles are currently project-only. The requested name, filename, and frontmatter `name` must match exactly. The profile must resolve to a regular file inside `.qwen/fork-profiles/` and cannot exceed 64 KiB.
- `tools` is required and follows the `fork_tools` rules, including empty-array deny-all behavior.
- `promptHint` is optional and limited to 200 characters. It is escaped and framed as project-supplied guidance after the fork directive and before the authoritative tool restriction; it does not change the inherited system instruction or model-visible tool declarations. Profile files are frontmatter-only, so non-blank Markdown after the closing `---` is rejected instead of silently ignored.
- The profile is resolved once at launch. A retained fork continues with the resolved tool snapshot even if the project file later changes.
- Project fork profiles are unavailable in safe mode and bare mode, which disable local customizations.

Like `fork_tools`, a fork profile is a caller-selected restriction rather than an administrator sandbox. Its optional prompt guidance is project-controlled content.

### How Fork Differs from Named Subagents

|               | Named Subagent                                                 | Fork Subagent                                                                                                                                                                                        |
| ------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Context       | Starts fresh with no parent conversation history               | Inherits all parent history by default; `fork_turns` can select a bounded recent window                                                                                                              |
| System prompt | Uses its own configured prompt                                 | Uses parent's exact system prompt (for cache sharing)                                                                                                                                                |
| Tools         | Configured declaration set without interactive question tools  | Keeps the parent-derived declaration set for caching; execution always rejects `ask_user_question`, and `fork_tools` or `fork_profile` can independently narrow it without changing that declaration |
| Execution     | Background by default; supports an explicit foreground opt-out | Always detached; parent continues immediately                                                                                                                                                        |
| Use case      | Specialized tasks (testing, docs)                              | Parallel tasks that need the current context                                                                                                                                                         |

### When Fork is Used

The AI automatically uses fork when it needs to:

- Run multiple research tasks in parallel (e.g., "investigate module A, B, and C")
- Perform background work while continuing the main conversation
- Delegate tasks that require understanding of the current conversation context

### Prompt Cache Sharing

All forks share the parent's exact API request prefix (system prompt, tools, conversation history), enabling DashScope prompt cache hits. When 3 forks run in parallel, the shared prefix is cached once and reused — saving 80%+ token costs compared to independent subagents.

### Recursive Delegation Prevention

Fork children cannot spawn any further sub-agent. This is enforced at runtime — if a fork calls the Agent tool, it receives an error instructing it to execute tasks directly.

### Current Limitation

- **No worktree isolation**: Forks share the parent's working directory. Concurrent file modifications from multiple forks may conflict.

## Key Benefits

- **Task Specialization**: Create agents optimized for specific workflows (testing, documentation, refactoring, etc.)
- **Context Isolation**: Keep specialized work separate from your main conversation
- **Context Inheritance**: Fork subagents inherit the full conversation by default and can select a bounded number of recent parent turns
- **Prompt Cache Sharing**: Fork subagents share the parent's cache prefix, reducing token costs
- **Reusability**: Save and reuse agent configurations across projects and sessions
- **Controlled Access**: Limit which tools each agent can use for security and focus
- **Progress Visibility**: Monitor agent execution with real-time progress updates

## How Subagents Work

1. **Configuration**: You create Subagents configurations that define their behavior, tools, and system prompts
2. **Delegation**: The main AI can automatically delegate tasks to appropriate Subagents — or fork itself (`subagent_type: "fork"`) when it needs the parent conversation context
3. **Execution**: Subagents work independently, using their configured tools to complete tasks
4. **Results**: Background runs send a completion notification containing the result to the main conversation; foreground regular subagents return results inline
5. **Continuation**: The main AI can use `list_agents` to find background agents and `send_message` to continue a running, paused, or completed agent

## Background Agent Continuation

Top-level regular subagents run in the background by default. After a background agent finishes, Qwen Code keeps enough state to continue related work without launching a duplicate agent:

- `list_agents` returns the addressable background agents in the current session, including compatible agents restored with a resumed session. Each entry includes a `task_id`, status, and whether it can receive a message.
- `send_message` with that `task_id` queues a message for a running agent, resumes a paused agent, or continues a completed agent. Completed agents reuse their resident runtime when available and otherwise revive from their retained transcript.
- A continued agent reports its next result through another completion notification.

When a session is restored, compatible background agents are added back to the session roster. A task can be visible but not continuable when its retained state is missing or incompatible; `list_agents` reports the reason in that case.

Use continuation for related follow-up work. Launch a new agent when the task is unrelated or the previous agent cannot be resumed.

## Notification Queue

In the interactive TUI and ACP session, completion notifications from background agents, shells, monitors and workflows share a queue that drains into a model turn once the session is idle. These queues hold at most 20 notifications so a noisy producer cannot accumulate an unbounded backlog. The headless CLI's local queue is not capped by this rule.

When a 21st notification arrives, Qwen Code evicts an interim monitor pulse first — the monitor's next poll supersedes it — and otherwise the oldest queued notification. Agent results, workflow results and scheduled prompts are never evicted in the interactive TUI; a notification that would displace one is dropped instead, and so is an arriving pulse when only terminal results are queued.

Discarded notifications are reported rather than dropped quietly. The summary appears before the next notification in the live transcript. ACP also prefixes it to that turn's model input; the TUI keeps it parked for the next Notification batch so cron prompts still pass unchanged through slash, shell and `@` preprocessing. A daemon notification is recorded before it is acknowledged, so after a reload its durable record can precede the later overflow summary. ACP can discard a pending summary if the session is cleared or switched, or if a client cancels or preempts the notification turn. Discarding a notification never stops or deletes its task, and completed tasks retain their results; the summary points at `/tasks` and task output files when there is a task to inspect. A discarded scheduled prompt was never delivered and is not retried. A daemon notification that was recorded but could not be delivered live remains available in the session transcript and is reported separately from lost notifications.

## Agent Working Directory

For a named regular subagent, `working_dir` pins the agent to an existing git worktree of the current repository. Relative paths resolve from the current directory, and the worktree must already be registered with git as a linked worktree of this repository.

`working_dir` cannot be combined with `subagent_type: "fork"`. An unnamed caller-owned `working_dir` launch runs in the foreground because Qwen Code does not own that worktree's lifecycle: an explicit `run_in_background: true` request is rejected, while a configured background default (`background: true` in a subagent definition) is rejected at the top level and downgraded to the foreground when nested. If both `working_dir` and `isolation: "worktree"` are supplied, Qwen Code reuses the caller-owned worktree instead of creating another one. Workflow scripts are deliberately stricter: a workflow `agent()` call that receives both `workingDir` and `isolation` is rejected rather than run with `isolation` ignored.

## Getting Started

### Quick Start

1. **Create your first Subagent**:

   `/agents create`

   Follow the guided wizard to create a specialized agent.

2. **Manage existing agents**:

   `/agents manage`

   View and manage your configured Subagents.

3. **Use Subagents automatically**: Simply ask the main AI to perform tasks that match your Subagents' specializations. The AI will automatically delegate appropriate work.

### Example Usage

```
User: "Please write comprehensive tests for the authentication module"
AI: I'll delegate this to your testing specialist Subagents.
[Delegates to "testing-expert" Subagents]
[Shows real-time progress of test creation]
[Returns with completed test files and execution summary]`
```

## Management

### CLI Commands

Subagents are managed through the `/agents` slash command and its subcommands:

**Usage:**：`/agents create`。Creates a new Subagent through a guided step wizard.

**Usage:**：`/agents manage`。Opens an interactive management dialog for viewing and managing existing Subagents.

### Storage Locations

Subagents are stored as Markdown files in multiple locations:

- **Project-level**: `.qwen/agents/` (highest precedence)
- **User-level**: `~/.qwen/agents/` (fallback)
- **Extension-level**: Provided by installed extensions

This allows you to have project-specific agents, personal agents that work across all projects, and extension-provided agents that add specialized capabilities.

### Extension Subagents

Extensions can provide custom subagents that become available when the extension is enabled. These agents are stored in the extension's `agents/` directory and follow the same format as personal and project agents.

Extension subagents:

- Are automatically discovered when the extension is enabled
- Appear in the `/agents manage` dialog under "Extension Agents" section
- Cannot be edited directly (edit the extension source instead)
- Follow the same configuration format as user-defined agents

To see which extensions provide subagents, check the extension's `qwen-extension.json` file for an `agents` field.

### File Format

Subagents are configured using Markdown files with YAML frontmatter. This format is human-readable and easy to edit with any text editor.

#### Basic Structure

```
---
name: agent-name
description: Brief description of when and how to use this agent
model: inherit # Optional: inherit, fast, modelId, or authType:modelId
approvalMode: auto-edit # Optional: default, plan, auto-edit, yolo, bubble
tools:         # Optional: allowlist of tools
  - tool1
  - tool2
disallowedTools: # Optional: blocklist of tools
  - tool3
---

System prompt content goes here.
Multiple paragraphs are supported.
```

#### Model Selection

Use the optional `model` frontmatter field to control which model a subagent uses:

- `inherit`: Use the same model as the main conversation.
- Omit the field: Same as `inherit`.
- `fast`: Use the configured `fastModel`. If no valid fast model is configured,
  the subagent falls back to `inherit`.
- `glm-5`: Use that model ID. Qwen Code first checks the main conversation's
  auth type; if the model is not available there, it can resolve the model from
  another configured provider.
- `openai:gpt-4o`: Use an explicit provider and model ID. This is useful when a
  subagent should run on a model registered under a different auth type from the
  main conversation.

For example:

```
---
name: fast-reviewer
description: Reviews small diffs with the configured fast model
model: fast
tools:
  - read_file
  - grep_search
---
```

```
---
name: openai-researcher
description: Uses an OpenAI-compatible provider for research tasks
model: openai:gpt-4o
tools:
  - read_file
  - grep_search
  - glob
---
```

The `fast` selector uses the same `fastModel` setting configured in
`settings.json` or with `/model --fast`. That setting may itself refer to a
model under another configured auth type, such as `openai:deepseek-v4-flash`.
When the selector resolves to another auth type, Qwen Code creates a dedicated
runtime provider for that subagent request and sends the provider only the bare
model ID.

The built-in Explore agent inherits the main session model by default. To
select a different model for only that built-in agent, configure
`agents.builtin.exploreModel` in `settings.json` and restart Qwen Code:

Earlier versions used `fastModel` for Explore by default. To preserve that
behavior, set `agents.builtin.exploreModel` to `fast`.

```json
{
  "agents": {
    "builtin": {
      "exploreModel": "fast"
    }
  }
}
```

This setting accepts the same selectors described above. It is applied only
when Qwen Code resolves the built-in Explore definition; a session, project,
user, or extension agent named Explore keeps its own `model` setting.

To let the model select from user-defined grades without exposing concrete
model IDs, configure `agents.modelGrades` and optionally restrict them with
`agents.allowedGrades`:

```json
{
  "agents": {
    "modelGrades": {
      "small": "fast",
      "high": "qwen-max"
    },
    "allowedGrades": ["small", "high"]
  }
}
```

The Agent tool then accepts `model: "small"` or `model: "high"` for regular
subagents. Unknown, disallowed, fork, and named-teammate grade selections are
rejected. A custom agent's explicit model still takes precedence over a grade.

#### Permission Mode

Use the optional `approvalMode` frontmatter field to control how a subagent's tool calls are approved. Valid values:

- `default`: Tools require interactive approval (same as the main session default)
- `plan`: Analyze-only mode — the agent plans but does not execute changes
- `auto-edit`: Tools are auto-approved without prompting (recommended for most agents)
- `yolo`: All tools auto-approved, including potentially destructive ones
- `bubble`: Background-agent tool approvals are surfaced in the parent session

If you omit this field, the subagent's permission mode is determined automatically:

- If the parent session is in **yolo** or **auto-edit** mode, the subagent inherits that mode. A permissive parent stays permissive.
- If the parent session is in **plan** mode, the subagent stays in plan mode. An analyze-only session cannot mutate files through a delegated agent.
- If the parent session is in **default** mode (in a trusted folder), the subagent gets **auto-edit** so it can work autonomously.

When you do set `approvalMode`, the parent's permissive modes still take priority. For example, if the parent is in yolo mode, a subagent with `approvalMode: plan` will still run in yolo mode.

```
---
name: cautious-reviewer
description: Reviews code without making changes
approvalMode: plan
tools:
  - read_file
  - grep_search
  - glob
---

You are a code reviewer. Analyze the code and report findings.
Do not modify any files.
```

#### Tool Configuration

Use `tools` and `disallowedTools` to control which tools a subagent can access.

**`tools` (allowlist):** When specified, the subagent can only use the listed tools. When omitted, the subagent inherits all available tools from the parent session.

The allowlist applies both to directly declared tools and to targets invoked through the `tool_search`/`tool_call` deferred-tool bridge. An explicit list of ordinary tools does not automatically include either bridge tool; name both `tool_search` and `tool_call` if the agent needs discovery and bridge invocation. A listed ordinary deferred target is declared directly, while a target demoted by `tools.eager` remains hidden unless a separate reveal rule applies. A hidden target must still be present in `tools` to be invoked through the bridge. `disallowedTools` and `permissions.deny` remain additional blocklists, and listing a tool does not bypass the subagent control-plane exclusions. Code Mode (`tools.codeModeOnly`) differs: `tool_call` is not available, an agent allowed any tool callable from `exec` gets `tool_search` automatically unless `disallowedTools` names it, and tools demoted by `tools.eager` stay callable through `exec`.

```
---
name: reader
description: Read-only agent for code exploration
tools:
  - read_file
  - grep_search
  - glob
  - web_fetch
---
```

**`disallowedTools` (blocklist):** When specified, the listed tools are removed from the subagent's tool pool. This is useful when you want "everything except X" without listing every permitted tool.

```
---
name: safe-worker
description: Agent that cannot modify files
disallowedTools:
  - write_file
  - edit
  - run_shell_command
---
```

If both `tools` and `disallowedTools` are set, the allowlist is applied first, then the blocklist removes from that set.

**MCP tools** follow the same rules. If a subagent has no `tools` list, it inherits all MCP tools from the parent session. If a subagent has an explicit `tools` list, it only gets MCP tools that are explicitly named in that list.

The `disallowedTools` field supports MCP server-level patterns:

- `mcp__server__tool_name` — blocks a specific MCP tool
- `mcp__server` — blocks all tools from that MCP server

```
---
name: no-slack
description: Agent without Slack access
disallowedTools:
  - mcp__slack
---
```

#### Claude Code Compatibility Fields

Qwen Code accepts the Claude Code 2.1.168 frontmatter fields below so you
can drop a CC agent file into `.qwen/agents/` and have the supported fields
parse identically. Optional fields with invalid values are silently dropped
at parse time rather than rejected — the same lenient posture CC uses.

| Field            | Type             | Notes                                                                                                                                                                                                                                                                            |
| ---------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `permissionMode` | enum string      | `acceptEdits`, `auto`, `bypassPermissions`, `default`, `dontAsk`, `plan`. Mapped to `approvalMode` at parse time; when both are set, the explicit `approvalMode` wins.                                                                                                           |
| `maxTurns`       | positive integer | Caps the agent's turn budget. Wired into `runConfig.max_turns` at runtime; when both are set, the top-level field wins. The legacy nested value is pruned from the on-disk file on save to avoid two sources of truth.                                                           |
| `color`          | enum string      | Display color. Allowlist: `red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan` (mirrors CC's `_Y`). The legacy qwen sentinel `auto` is preserved for backward compatibility. Other values are silently dropped on parse.                                         |
| `mcpServers`     | record of specs  | Per-agent MCP server overrides. Merged with the session-level MCP server set when the agent spawns; on key collision the agent's spec wins (matching CC's `scope: 'agent'` semantics). Malformed entries are dropped per-key with a warning rather than failing the whole agent. |
| `hooks`          | record of arrays | Per-agent hooks. Keys are CC hook event names (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, …); values are arrays of `{ matcher?, hooks: [...] }` definitions in the same shape as `settings.json`'s `hooks` field. Registered while the agent runs, removed when it stops.  |

Example with all of the above:

```
---
name: rigorous-reviewer
description: Deep code review with a turn cap
permissionMode: plan
maxTurns: 50
color: cyan
tools:
  - read_file
  - grep_search
  - glob
mcpServers:
  filesystem:
    type: stdio
    command: node
    args: [/usr/local/lib/mcp-fs/server.js]
hooks:
  PreToolUse:
    - matcher: Bash
      hooks:
        - type: command
          command: echo "review-agent about to run a shell command"
---

You are a code reviewer. Analyze the code thoroughly and report findings
ordered by severity.
```

The remaining CC frontmatter fields — `effort`, `skills`, `initialPrompt`,
`memory`, `isolation` — are documented in the declarative-agent design doc
and land in follow-up PRs once the prerequisite infrastructure exists
(`effort` needs a model-layer parameter; `memory` needs a scoped memory
subsystem; `--agent` CLI flag enables `initialPrompt`; etc.).

Agent frontmatter hooks run only for that agent invocation. They do not receive
parent, sibling, or nested child agent events. Global settings hooks and
session-wide skill/function hooks keep their existing inheritance. Hooks are
removed when the invocation is disposed. Project-agent hooks are skipped in an
untrusted workspace and recheck their source workspace's trust before each event.

Use `SubagentStart` and `SubagentStop` for the agent's lifecycle. `Stop` is not an
alias for `SubagentStop`; hooks that previously observed the parent's `Stop` or
other agents through shared registration must move to settings to keep that
session-wide behavior.

#### Example Usage

```
---
name: project-documenter
description: Creates project documentation and README files
---

You are a documentation specialist.

Focus on creating clear, comprehensive documentation that helps both
new contributors and end users understand the project.
```

## Using Subagents Effectively

### Automatic Delegation

Qwen Code proactively delegates tasks based on:

- The task description in your request
- The description field in Subagents configurations
- Current context and available tools

To encourage more proactive Subagents use, include phrases like "use PROACTIVELY" or "MUST BE USED" in your description field.

### Explicit Invocation

Request a specific Subagent by mentioning it in your command:

```
Let the testing-expert Subagents create unit tests for the payment module
Have the documentation-writer Subagents update the API reference
Get the react-specialist Subagents to optimize this component's performance
```

## Examples

### Development Workflow Agents

#### Testing Specialist

Perfect for comprehensive test creation and test-driven development.

```
---
name: testing-expert
description: Writes comprehensive unit tests, integration tests, and handles test automation with best practices
tools:
  - read_file
  - write_file
  - read_many_files
  - run_shell_command
---

You are a testing specialist focused on creating high-quality, maintainable tests.

Your expertise includes:

- Unit testing with appropriate mocking and isolation
- Integration testing for component interactions
- Test-driven development practices
- Edge case identification and comprehensive coverage
- Performance and load testing when appropriate

For each testing task:

1. Analyze the code structure and dependencies
2. Identify key functionality, edge cases, and error conditions
3. Create comprehensive test suites with descriptive names
4. Include proper setup/teardown and meaningful assertions
5. Add comments explaining complex test scenarios
6. Ensure tests are maintainable and follow DRY principles

Always follow testing best practices for the detected language and framework.
Focus on both positive and negative test cases.
```

**Use Cases:**

- “Write unit tests for the authentication service”
- “Create integration tests for the payment processing workflow”
- “Add test coverage for edge cases in the data validation module”

#### Documentation Writer

Specialized in creating clear, comprehensive documentation.

```
---
name: documentation-writer
description: Creates comprehensive documentation, README files, API docs, and user guides
tools:
  - read_file
  - write_file
  - read_many_files
---

You are a technical documentation specialist.

Your role is to create clear, comprehensive documentation that serves both
developers and end users. Focus on:

**For API Documentation:**

- Clear endpoint descriptions with examples
- Parameter details with types and constraints
- Response format documentation
- Error code explanations
- Authentication requirements

**For User Documentation:**

- Step-by-step instructions with screenshots when helpful
- Installation and setup guides
- Configuration options and examples
- Troubleshooting sections for common issues
- FAQ sections based on common user questions

**For Developer Documentation:**

- Architecture overviews and design decisions
- Code examples that actually work
- Contributing guidelines
- Development environment setup

Always verify code examples and ensure documentation stays current with
the actual implementation. Use clear headings, bullet points, and examples.
```

**Use Cases:**

- “Create API documentation for the user management endpoints”
- “Write a comprehensive README for this project”
- “Document the deployment process with troubleshooting steps”

#### Code Reviewer

Focused on code quality, security, and best practices.

```
---
name: code-reviewer
description: Reviews code for best practices, security issues, performance, and maintainability
tools:
  - read_file
  - read_many_files
---

You are an experienced code reviewer focused on quality, security, and maintainability.

Review criteria:

- **Code Structure**: Organization, modularity, and separation of concerns
- **Performance**: Algorithmic efficiency and resource usage
- **Security**: Vulnerability assessment and secure coding practices
- **Best Practices**: Language/framework-specific conventions
- **Error Handling**: Proper exception handling and edge case coverage
- **Readability**: Clear naming, comments, and code organization
- **Testing**: Test coverage and testability considerations

Provide constructive feedback with:

1. **Critical Issues**: Security vulnerabilities, major bugs
2. **Important Improvements**: Performance issues, design problems
3. **Minor Suggestions**: Style improvements, refactoring opportunities
4. **Positive Feedback**: Well-implemented patterns and good practices

Focus on actionable feedback with specific examples and suggested solutions.
Prioritize issues by impact and provide rationale for recommendations.
```

**Use Cases:**

- “Review this authentication implementation for security issues”
- “Check the performance implications of this database query logic”
- “Evaluate the code structure and suggest improvements”

### Technology-Specific Agents

#### React Specialist

Optimized for React development, hooks, and component patterns.

```
---
name: react-specialist
description: Expert in React development, hooks, component patterns, and modern React best practices
tools:
  - read_file
  - write_file
  - read_many_files
  - run_shell_command
---

You are a React specialist with deep expertise in modern React development.

Your expertise covers:

- **Component Design**: Functional components, custom hooks, composition patterns
- **State Management**: useState, useReducer, Context API, and external libraries
- **Performance**: React.memo, useMemo, useCallback, code splitting
- **Testing**: React Testing Library, Jest, component testing strategies
- **TypeScript Integration**: Proper typing for props, hooks, and components
- **Modern Patterns**: Suspense, Error Boundaries, Concurrent Features

For React tasks:

1. Use functional components and hooks by default
2. Implement proper TypeScript typing
3. Follow React best practices and conventions
4. Consider performance implications
5. Include appropriate error handling
6. Write testable, maintainable code

Always stay current with React best practices and avoid deprecated patterns.
Focus on accessibility and user experience considerations.
```

**Use Cases:**

- “Create a reusable data table component with sorting and filtering”
- “Implement a custom hook for API data fetching with caching”
- “Refactor this class component to use modern React patterns”

#### Python Expert

Specialized in Python development, frameworks, and best practices.

```
---
name: python-expert
description: Expert in Python development, frameworks, testing, and Python-specific best practices
tools:
  - read_file
  - write_file
  - read_many_files
  - run_shell_command
---

You are a Python expert with deep knowledge of the Python ecosystem.

Your expertise includes:

- **Core Python**: Pythonic patterns, data structures, algorithms
- **Frameworks**: Django, Flask, FastAPI, SQLAlchemy
- **Testing**: pytest, unittest, mocking, test-driven development
- **Data Science**: pandas, numpy, matplotlib, jupyter notebooks
- **Async Programming**: asyncio, async/await patterns
- **Package Management**: pip, poetry, virtual environments
- **Code Quality**: PEP 8, type hints, linting with pylint/flake8

For Python tasks:

1. Follow PEP 8 style guidelines
2. Use type hints for better code documentation
3. Implement proper error handling with specific exceptions
4. Write comprehensive docstrings
5. Consider performance and memory usage
6. Include appropriate logging
7. Write testable, modular code

Focus on writing clean, maintainable Python code that follows community standards.
```

**Use Cases:**

- “Create a FastAPI service for user authentication with JWT tokens”
- “Implement a data processing pipeline with pandas and error handling”
- “Write a CLI tool using argparse with comprehensive help documentation”

## Best Practices

### Design Principles

#### Single Responsibility Principle

Each Subagent should have a clear, focused purpose.

**✅ Good:**

```
---
name: testing-expert
description: Writes comprehensive unit tests and integration tests
---
```

**❌ Avoid:**

```
---
name: general-helper
description: Helps with testing, documentation, code review, and deployment
---
```

**Why:** Focused agents produce better results and are easier to maintain.

#### Clear Specialization

Define specific expertise areas rather than broad capabilities.

**✅ Good:**

```
---
name: react-performance-optimizer
description: Optimizes React applications for performance using profiling and best practices
---
```

**❌ Avoid:**

```
---
name: frontend-developer
description: Works on frontend development tasks
---
```

**Why:** Specific expertise leads to more targeted and effective assistance.

#### Actionable Descriptions

Write descriptions that clearly indicate when to use the agent.

**✅ Good:**

```
description: Reviews code for security vulnerabilities, performance issues, and maintainability concerns
```

**❌ Avoid:**

```
description: A helpful code reviewer
```

**Why:** Clear descriptions help the main AI choose the right agent for each task.

### Configuration Best Practices

#### System Prompt Guidelines

**Be Specific About Expertise:**

```
You are a Python testing specialist with expertise in:

- pytest framework and fixtures
- Mock objects and dependency injection
- Test-driven development practices
- Performance testing with pytest-benchmark
```

**Include Step-by-Step Approaches:**

```
For each testing task:

1. Analyze the code structure and dependencies
2. Identify key functionality and edge cases
3. Create comprehensive test suites with clear naming
4. Include setup/teardown and proper assertions
5. Add comments explaining complex test scenarios
```

**Specify Output Standards:**

```
Always follow these standards:

- Use descriptive test names that explain the scenario
- Include both positive and negative test cases
- Add docstrings for complex test functions
- Ensure tests are independent and can run in any order
```

## Security Considerations

- **Tool Restrictions**: Use `tools` to limit which tools a subagent can access, or `disallowedTools` to block specific tools while inheriting everything else
- **Permission Mode**: Subagents inherit their parent's permission mode by default. Plan-mode sessions cannot escalate to auto-edit through delegated agents. Privileged modes (auto-edit, yolo) are blocked in untrusted folders.
- **Provider Selection**: A subagent with `model: authType:modelId`, or
  `model: fast` where `fastModel` resolves to another auth type, sends that
  subagent's model requests to the selected provider. Make sure that provider is
  appropriate for the subagent's task and data.
- **Sandboxing**: All tool execution follows the same security model as direct tool use
- **Audit Trail**: All Subagents actions are logged and visible in real-time
- **Access Control**: Project and user-level separation provides appropriate boundaries
- **Sensitive Information**: Avoid including secrets or credentials in agent configurations
- **Production Environments**: Consider separate agents for production vs development environments

## Limits

The following soft warnings apply to Subagent configurations (no hard limits are enforced):

- **Description Field**: A warning is shown for descriptions exceeding 1,000 characters
- **System Prompt**: A warning is shown for system prompts exceeding 10,000 characters

---

### Users Features Tips (`docs/users/features/tips.md`)

# Contextual Tips

Qwen Code includes a contextual tips system that helps you discover features and stay aware of session state.

## Startup Tips

Each time you launch Qwen Code, a tip is shown in the header area. Tips are selected by priority first, then rotated across sessions using LRU (least-recently-used) scheduling among tips of the same priority, so you see a different tip each time.

New users see onboarding-focused tips during their first sessions:

| Sessions | Example tips                                         |
| -------- | ---------------------------------------------------- |
| < 5      | Slash commands (`/`), Tab autocomplete               |
| < 10     | `QWEN.md` project context, `--continue` / `--resume` |
| < 15     | Shell commands with `!` prefix                       |

After that, tips rotate through general features like `/compress`, `/approval-mode`, `/insight`, `/btw`, and more.

## Post-Response Tips

During a conversation, Qwen Code monitors your context window usage and shows tips when action may be needed:

| Context usage | Condition                      | Tip                                               |
| ------------- | ------------------------------ | ------------------------------------------------- |
| 50-80%        | After a few prompts in session | Suggests `/compress` to free up context           |
| 80-95%        | —                              | Warns context is getting full                     |
| >= 95%        | —                              | Urgent: run `/compress` now or `/new` to continue |

Post-response tips have per-tip cooldowns to avoid being repetitive.

## Tip History

Tip display history is persisted at `~/.qwen/tip_history.json`. This file tracks:

- Session count (used for new-user tip selection)
- Which tips have been shown and when (used for LRU rotation and cooldown)

You can safely delete this file to reset tip history.

## Disabling Tips

To hide all tips (both startup and post-response), set `ui.hideTips` to `true` in `~/.qwen/settings.json`:

```json
{
  "ui": {
    "hideTips": true
  }
}
```

You can also toggle this in the settings dialog via the `/settings` command.

Tips are also automatically hidden when screen reader mode is enabled.

---

### Users Features Token Caching (`docs/users/features/token-caching.md`)

# Token Caching and Cost Optimization

Caching lowers the price of the prefix every request carries. To make that prefix smaller in the first place — fewer resident tool schemas, smaller context files — see [Resident Context Cost](context-cost.md); the two compose.

Qwen Code automatically optimizes API costs through token caching when using API key authentication. This feature stores frequently used content like system instructions and conversation history to reduce the number of tokens processed in subsequent requests.

## How It Benefits You

- **Cost reduction**: Less tokens mean lower API costs
- **Faster responses**: Cached content is retrieved more quickly
- **Automatic optimization**: No configuration needed - it works behind the scenes

## Token caching is available for

- API key users (Qwen API key, OpenAI-compatible providers)

## Monitoring Your Savings

Use the `/stats` command to see your cached token savings:

- When active, the stats display shows how many tokens were served from cache
- You'll see both the absolute number and percentage of cached tokens
- Example: "10,500 (90.4%) of input tokens were served from the cache, reducing costs."

This information is only displayed when cached tokens are being used, which occurs with API key authentication but not with OAuth authentication.

## Example Stats Display



The above image shows an example of the `/stats` command output, highlighting the cached token savings information.

---

### Users Features Tool Use Summaries (`docs/users/features/tool-use-summaries.md`)

# Tool-Use Summaries

Qwen Code can generate a short, git-commit-subject-style label after each tool batch completes, summarizing what the batch accomplished. The label appears inline: for a completed tool group in the main view it replaces the generic `Tool × N` header; when the group is force-expanded (in `Ctrl+O` expanded detail mode, or for error / user-initiated batches) it appears as a dim `● <label>` line below the group.

This is a UX aid for parallel tool calls: when the model fans out into several `Read` + `Grep` + `Bash` calls at once, the summary tells you the intent at a glance instead of forcing you to scan the tool list.

The feature is enabled by default and runs silently in the background. It requires a configured [fast model](./followup-suggestions#fast-model).

## What You See

### Main view (completed group)

In the main transcript, a completed collapsible batch folds into a single labeled row — the summary replaces the generic `Tool × N` header:

```
╭──────────────────────────────────────────────╮
│✓  Read 4 text files                          │
╰──────────────────────────────────────────────╯
```

The full per-tool output is a keystroke away: press `Ctrl+O` to toggle expanded detail mode.

### Expanded detail mode (`Ctrl+O`) and force-expanded groups

When a group is force-expanded — in `Ctrl+O` expanded detail mode, or for error / user-initiated batches in the main view — each tool renders individually and the summary appears as a dim badge line below the group:

```
╭──────────────────────────────────────────────╮
│ ✓  ReadFile a.txt                            │
│ ✓  ReadFile b.txt                            │
│ ✓  ReadFile c.txt                            │
│ ✓  ReadFile d.txt                            │
╰──────────────────────────────────────────────╯

 ● Read 4 text files
```

## How It Works

After a tool batch finalizes, Qwen Code fires a fire-and-forget call to the configured fast model with:

- The tool names, truncated arguments, and truncated results (each capped at 300 characters).
- The assistant's most recent text output (first 200 characters) as an intent prefix.
- A system prompt instructing the model to return a past-tense, 30-character label in git-commit-subject style.

The call runs in parallel with the next turn's API streaming, so its ~1s latency is hidden behind the main model's response. When the label resolves, it is appended to the transcript as a `tool_use_summary` entry.

Example labels: `Searched in auth/`, `Fixed NPE in UserService`, `Created signup endpoint`, `Read config.json`, `Ran failing tests`.

## When It Appears

The summary is generated when **all** of the following are true:

- `experimental.emitToolUseSummaries` is `true` (default).
- A `fastModel` is configured (via settings or `/model --fast`).
- At least one tool completed in the batch.
- The turn was not aborted before tool completion.
- The fast model returned a non-empty, non-error response.

Subagent tool calls do not trigger summary generation — only the main session's tool batches do.

## When It Doesn't Appear

The summary is silently skipped (no error, no UI change) when:

- No fast model is configured.
- The fast model call fails, times out, or returns empty.
- The model returned an obvious error-message-like string (e.g., `Error: ...`, `I cannot ...`) — filtered out by the client so the UI does not show misleading labels.
- The turn was aborted (`Ctrl+C`) before the model finished.

In all these cases, the tool group renders as it always has.

## Fast Model

The label is generated using the [fast model](./followup-suggestions#fast-model) — the same model you configure for prompt suggestions and speculative execution. Configure it via:

### Via command

```
/model --fast qwen3-coder-flash
```

### Via `settings.json`

```json
{
  "fastModel": "qwen3-coder-flash"
}
```

When no fast model is configured, summary generation is skipped entirely — the feature has no effect until you set one up.

## Configuration

These settings can be configured in `settings.json`:

| Setting                             | Type    | Default | Description                                                                                        |
| ----------------------------------- | ------- | ------- | -------------------------------------------------------------------------------------------------- |
| `experimental.emitToolUseSummaries` | boolean | `true`  | Master switch for summary generation. Turn off to disable the extra fast-model call.               |
| `fastModel`                         | string  | `""`    | Fast model used for summary generation (shared with prompt suggestions). Required; no-op if empty. |

### Environment override

`QWEN_CODE_EMIT_TOOL_USE_SUMMARIES` overrides the `experimental.emitToolUseSummaries` setting for the current session:

- `QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0` or `=false` — force off.
- `QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=1` or `=true` — force on.
- Unset — use the `experimental.emitToolUseSummaries` setting.

### Example

```json
{
  "fastModel": "qwen3-coder-flash",
  "experimental": {
    "emitToolUseSummaries": true
  }
}
```

## Scope & lifecycle

Three points that tend to trip up a first read of this feature:

1. **One generation per batch, shared by both display modes.** The fast-model call happens exactly once in `handleCompletedTools` when a tool batch finalizes. Toggling `Ctrl+O` expanded detail mode afterwards does **not** trigger a new call — both the collapsed and the expanded rendering read from the same `tool_use_summary` history entry that was captured the first time.
2. **No backfill on toggle or on session resume.** A `tool_group` that completed before the feature was enabled (or before you flipped the setting on, or in a resumed session — `ChatRecordingService` does not persist summary entries) will never get a label. There is no "sweep existing history" pass. If you turn this setting on mid-session, only _future_ batches will show a label; older groups keep the default rendering with no indicator that a label is missing.
3. **Main-agent batches only.** The trigger lives in the main session's turn loop (`useLlmStream`), so:
   - ✅ Shell, MCP, file operations, and the `Task` / subagent tool _call itself_ (as it appears in the main batch) are summarized.
   - ❌ A subagent's **internal** tool batches (run through `packages/core/src/agents/runtime/`) are not summarized.

   An outer batch that _contains_ a `Task` tool will still be labeled, but the fast model sees only the subagent tool call and its aggregated output — not the individual tool calls inside the subagent. Expect labels like `Ran research-agent` or `Delegated file search` rather than `Searched 14 files`. This is intentional — summarizing subagent internals would multiply the fast-model cost and surface noise that never shows up in the primary UI.

## Display behavior

The main view already folds a completed collapsible batch into a single labeled row (`✓  Read 4 text files`) — the summary does the work of the old per-tool list. For full per-tool detail, press `Ctrl+O` to toggle expanded detail mode, where each tool renders individually with the summary as a trailing `● <label>` line below the group.

```json
{
  "fastModel": "qwen3-coder-flash",
  "experimental": {
    "emitToolUseSummaries": true
  }
}
```

For small same-type batches (e.g. `Read × 3`) the expanded `● <label>` line can read as a restatement of the visible tool lines; if that matches your usual workflow you can turn the summary off entirely via `experimental.emitToolUseSummaries: false`.

## Monitoring

Summary model usage appears in `/stats` output under the fast-model token totals, with the `prompt_id` `tool_use_summary_generation` so it can be distinguished from prompt suggestions and other background tasks.

## Data flow & privacy

The summary call sends each successful tool's name, truncated `args`, and truncated result (each field capped at 300 characters) to the **fast model**, plus the first 200 characters of the assistant's most recent text as an intent prefix.

If your fast model is configured for the same provider/auth as your main session model, the data flows along the same boundary your main session already uses — no change in trust scope. If you have configured a fast model from a **different provider**, tool inputs and outputs (potentially including file contents read by `read_file`, command output from shell calls, or values surfaced through MCP tools) will be sent to that other provider as part of the summarization prompt. That is a strictly larger data-sharing scope than the main session alone.

If this matters for your workflow, you have two clean options:

- Configure `fastModel` to a model under the same provider as your main session, so the summary call doesn't cross any new auth/data boundary.
- Disable the feature entirely with `experimental.emitToolUseSummaries: false` (or `QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0`).

The 300-character per-field cap limits exposure but does not eliminate it — secrets discovered in tool output during the cap window can still be sent. Treat the fast model's data boundary the same way you treat the main model's.

## Cost

One fast-model call per qualifying tool batch. Input is a small fixed system prompt plus the truncated tool inputs/outputs (each capped at 300 characters per field). Output is a single short line (capped at 100 characters, typically 20 tokens or fewer). On a typical fast model this is roughly $0.001 per batch.

If you do not want the extra cost, turn the feature off via `experimental.emitToolUseSummaries: false` or `QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0`.

## Related

- [Expanded detail mode](../configuration/settings#ui) — press `Ctrl+O` to expand all tool outputs inline; the summary replaces the generic tool-group header for completed groups in the main view.
- [Followup Suggestions](./followup-suggestions) — another fast-model-driven UX enhancement that shares the same `fastModel` setting.

---

### Users Ide Integration Ide Companion Spec (`docs/users/ide-integration/ide-companion-spec.md`)

# Qwen Code Companion Plugin: Interface Specification

> Last Updated: September 15, 2025

This document defines the contract for building a companion plugin to enable Qwen Code's IDE mode. For VS Code, these features (native diffing, context awareness) are provided by the official extension ([marketplace](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion)). This specification is for contributors who wish to bring similar functionality to other editors like JetBrains IDEs, Sublime Text, etc.

## I. The Communication Interface

Qwen Code and the IDE plugin communicate through a local communication channel.

### 1. Transport Layer: MCP over HTTP

The plugin **MUST** run a local HTTP server that implements the **Model Context Protocol (MCP)**.

- **Protocol:** The server must be a valid MCP server. We recommend using an existing MCP SDK for your language of choice if available.
- **Endpoint:** The server should expose a single endpoint (e.g., `/mcp`) for all MCP communication.
- **Port:** The server **MUST** listen on a dynamically assigned port (i.e., listen on port `0`).

### 2. Discovery Mechanism: The Lock File

For Qwen Code to connect, it needs to discover what port your server is using. The plugin **MUST** facilitate this by creating a "lock file" and setting the port environment variable.

- **How the CLI Finds the File:** The CLI reads the port from `QWEN_CODE_IDE_SERVER_PORT`, then reads `~/.qwen/ide/<PORT>.lock`. (Legacy fallbacks exist for older extensions; see note below.)
- **File Location:** The file must be created in a specific directory: `~/.qwen/ide/`. Your plugin must create this directory if it doesn't exist.
- **File Naming Convention:** The filename is critical and **MUST** follow the pattern:
  `<PORT>.lock`
  - `<PORT>`: The port your MCP server is listening on.
- **File Content & Workspace Validation:** The file **MUST** contain a JSON object with the following structure:

  ```json
  {
    "port": 12345,
    "workspacePath": "/path/to/project1:/path/to/project2",
    "authToken": "a-very-secret-token",
    "ppid": 1234,
    "ideName": "VS Code"
  }
  ```
  - `port` (number, required): The port of the MCP server.
  - `workspacePath` (string, required): A list of all open workspace root paths, delimited by the OS-specific path separator (`:` for Linux/macOS, `;` for Windows). The CLI uses this path to ensure it's running in the same project folder that's open in the IDE. If the CLI's current working directory is not a sub-directory of `workspacePath`, the connection will be rejected. Your plugin **MUST** provide the correct, absolute path(s) to the root of the open workspace(s).
  - `authToken` (string, required): A secret token for securing the connection. The CLI will include this token in an `Authorization: Bearer <token>` header on all requests.
  - `ppid` (number, required): The parent process ID of the IDE process.
  - `ideName` (string, required): A user-friendly name for the IDE (e.g., `VS Code`, `JetBrains IDE`).

- **Authentication:** To secure the connection, the plugin **MUST** generate a unique, secret token and include it in the discovery file. The CLI will then include this token in the `Authorization` header for all requests to the MCP server (e.g., `Authorization: Bearer a-very-secret-token`). Your server **MUST** validate this token on every request and reject any that are unauthorized.
- **Environment Variables (Required):** Your plugin **MUST** set `QWEN_CODE_IDE_SERVER_PORT` in the integrated terminal so the CLI can locate the correct `<PORT>.lock` file.

**Legacy note:** For extensions older than v0.5.1, Qwen Code may fall back to reading JSON files in the system temp directory named `qwen-code-ide-server-<PID>.json` or `qwen-code-ide-server-<PORT>.json`. New integrations should not rely on these legacy files.

## II. The Context Interface

To enable context awareness, the plugin **MAY** provide the CLI with real-time information about the user's activity in the IDE.

### `ide/contextUpdate` Notification

The plugin **MAY** send an `ide/contextUpdate` [notification](https://modelcontextprotocol.io/specification/2025-06-18/basic/index#notifications) to the CLI whenever the user's context changes.

- **Triggering Events:** This notification should be sent (with a recommended debounce of 50ms) when:
  - A file is opened, closed, or focused.
  - The user's cursor position or text selection changes in the active file.
- **Payload (`IdeContext`):** The notification parameters **MUST** be an `IdeContext` object:

  ```typescript
  interface IdeContext {
    workspaceState?: {
      openFiles?: File[];
      isTrusted?: boolean;
    };
  }

  interface File {
    // Absolute path to the file
    path: string;
    // Last focused Unix timestamp (for ordering)
    timestamp: number;
    // True if this is the currently focused file
    isActive?: boolean;
    cursor?: {
      // 1-based line number
      line: number;
      // 1-based character number
      character: number;
    };
    // The text currently selected by the user
    selectedText?: string;
  }
  ```

  **Note:** The `openFiles` list should only include files that exist on disk. Virtual files (e.g., unsaved files without a path, editor settings pages) **MUST** be excluded.

### How the CLI Uses This Context

After receiving the `IdeContext` object, the CLI performs several normalization and truncation steps before sending the information to the model.

- **File Ordering:** The CLI uses the `timestamp` field to determine the most recently used files. It sorts the `openFiles` list based on this value. Therefore, your plugin **MUST** provide an accurate Unix timestamp for when a file was last focused.
- **Active File:** The CLI considers only the most recent file (after sorting) to be the "active" file. It will ignore the `isActive` flag on all other files and clear their `cursor` and `selectedText` fields. Your plugin should focus on setting `isActive: true` and providing cursor/selection details only for the currently focused file.
- **Truncation:** To manage token limits, the CLI truncates both the file list (to 10 files) and the `selectedText` (to 16KB).

While the CLI handles the final truncation, it is highly recommended that your plugin also limits the amount of context it sends.

## III. The Diffing Interface

To enable interactive code modifications, the plugin **MAY** expose a diffing interface. This allows the CLI to request that the IDE open a diff view, showing proposed changes to a file. The user can then review, edit, and ultimately accept or reject these changes directly within the IDE.

### `openDiff` Tool

The plugin **MUST** register an `openDiff` tool on its MCP server.

- **Description:** This tool instructs the IDE to open a modifiable diff view for a specific file.
- **Request (`OpenDiffRequest`):** The tool is invoked via a `tools/call` request. The `arguments` field within the request's `params` **MUST** be an `OpenDiffRequest` object.

  ```typescript
  interface OpenDiffRequest {
    // The absolute path to the file to be diffed.
    filePath: string;
    // The proposed new content for the file.
    newContent: string;
  }
  ```

- **Response (`CallToolResult`):** The tool **MUST** immediately return a `CallToolResult` to acknowledge the request and report whether the diff view was successfully opened.
  - On Success: If the diff view was opened successfully, the response **MUST** contain empty content (i.e., `content: []`).
  - On Failure: If an error prevented the diff view from opening, the response **MUST** have `isError: true` and include a `TextContent` block in the `content` array describing the error.

  The actual outcome of the diff (acceptance or rejection) is communicated asynchronously via notifications.

### `closeDiff` Tool

The plugin **MUST** register a `closeDiff` tool on its MCP server.

- **Description:** This tool instructs the IDE to close an open diff view for a specific file.
- **Request (`CloseDiffRequest`):** The tool is invoked via a `tools/call` request. The `arguments` field within the request's `params` **MUST** be an `CloseDiffRequest` object.

  ```typescript
  interface CloseDiffRequest {
    // The absolute path to the file whose diff view should be closed.
    filePath: string;
  }
  ```

- **Response (`CallToolResult`):** The tool **MUST** return a `CallToolResult`.
  - On Success: If the diff view was closed successfully, the response **MUST** include a single **TextContent** block in the content array containing the file's final content before closing.
  - On Failure: If an error prevented the diff view from closing, the response **MUST** have `isError: true` and include a `TextContent` block in the `content` array describing the error.

### `ide/diffAccepted` Notification

When the user accepts the changes in a diff view (e.g., by clicking an "Apply" or "Save" button), the plugin **MUST** send an `ide/diffAccepted` notification to the CLI.

- **Payload:** The notification parameters **MUST** include the file path and the final content of the file. The content may differ from the original `newContent` if the user made manual edits in the diff view.

  ```typescript
  {
    // The absolute path to the file that was diffed.
    filePath: string;
    // The full content of the file after acceptance.
    content: string;
  }
  ```

### `ide/diffRejected` Notification

When the user rejects the changes (e.g., by closing the diff view without accepting), the plugin **MUST** send an `ide/diffRejected` notification to the CLI.

- **Payload:** The notification parameters **MUST** include the file path of the rejected diff.

  ```typescript
  {
    // The absolute path to the file that was diffed.
    filePath: string;
  }
  ```

## IV. The Lifecycle Interface

The plugin **MUST** manage its resources and the discovery file correctly based on the IDE's lifecycle.

- **On Activation (IDE startup/plugin enabled):**
  1.  Start the MCP server.
  2.  Create the discovery file.
- **On Deactivation (IDE shutdown/plugin disabled):**
  1.  Stop the MCP server.
  2.  Delete the discovery file.

---

### Users Ide Integration Ide Integration (`docs/users/ide-integration/ide-integration.md`)

# IDE Integration

Qwen Code can integrate with your IDE to provide a more seamless and context-aware experience. This integration allows the CLI to understand your workspace better and enables powerful features like native in-editor diffing.

Currently, the only supported IDE is [Visual Studio Code](https://code.visualstudio.com/) and other editors that support VS Code extensions. To build support for other editors, see the [IDE Companion Extension Spec](../ide-integration/ide-companion-spec).

## Features

- **Workspace Context:** The CLI automatically gains awareness of your workspace to provide more relevant and accurate responses. This context includes:
  - The **10 most recently accessed files** in your workspace.
  - Your active cursor position.
  - Any text you have selected (up to a 16KB limit; longer selections will be truncated).

- **Native Diffing:** When Qwen suggests code modifications, you can view the changes directly within your IDE's native diff viewer. This allows you to review, edit, and accept or reject the suggested changes seamlessly.

- **VS Code Commands:** You can access Qwen Code features directly from the VS Code Command Palette (`Cmd+Shift+P` or `Ctrl+Shift+P`):
  - `Qwen Code: Run`: Starts a new Qwen Code session in the integrated terminal.
  - `Qwen Code: Accept Diff`: Accepts the changes in the active diff editor.
  - `Qwen Code: Close Diff Editor`: Rejects the changes and closes the active diff editor.
  - `Qwen Code: View Third-Party Notices`: Displays the third-party notices for the extension.

## Installation and Setup

There are three ways to set up the IDE integration:

### 1. Automatic Nudge (Recommended)

When you run Qwen Code inside a supported editor, it will automatically detect your environment and prompt you to connect. Answering "Yes" will automatically run the necessary setup, which includes installing the companion extension and enabling the connection.

### 2. Manual Installation from CLI

If you previously dismissed the prompt or want to install the extension manually, you can run the following command inside Qwen Code:

```
/ide install
```

This will find the correct extension for your IDE and install it.

### 3. Manual Installation from a Marketplace

You can also install the extension directly from a marketplace.

- **For Visual Studio Code:** Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion).
- **For VS Code Forks:** To support forks of VS Code, the extension is also published on the [Open VSX Registry](https://open-vsx.org/extension/qwenlm/qwen-code-vscode-ide-companion). Follow your editor's instructions for installing extensions from this registry.

> NOTE:
> The "Qwen Code Companion" extension may appear towards the bottom of search results. If you don't see it immediately, try scrolling down or sorting by "Newly Published".
>
> After manually installing the extension, you must run `/ide enable` in the CLI to activate the integration.

## Usage

### Enabling and Disabling

You can control the IDE integration from within the CLI:

- To enable the connection to the IDE, run:
  ```
  /ide enable
  ```
- To disable the connection, run:
  ```
  /ide disable
  ```

When enabled, Qwen Code will automatically attempt to connect to the IDE companion extension.

### Checking the Status

To check the connection status and see the context the CLI has received from the IDE, run:

```
/ide status
```

If connected, this command will show the IDE it's connected to and a list of recently opened files it is aware of.

(Note: The file list is limited to 10 recently accessed files within your workspace and only includes local files on disk.)

### Working with Diffs

When you ask Qwen model to modify a file, it can open a diff view directly in your editor.

**To accept a diff**, you can perform any of the following actions:

- Click the **checkmark icon** in the diff editor's title bar.
- Save the file (e.g., with `Cmd+S` or `Ctrl+S`).
- Open the Command Palette and run **Qwen Code: Accept Diff**.
- Respond with `yes` in the CLI when prompted.

**To reject a diff**, you can:

- Click the **'x' icon** in the diff editor's title bar.
- Close the diff editor tab.
- Open the Command Palette and run **Qwen Code: Close Diff Editor**.
- Respond with `no` in the CLI when prompted.

You can also **modify the suggested changes** directly in the diff view before accepting them.

If you select ‘Yes, allow always’ in the CLI, changes will no longer show up in the IDE as they will be auto-accepted.

## Using with Sandboxing

If you are using Qwen Code within a sandbox, please be aware of the following:

- **On macOS:** The IDE integration requires network access to communicate with the IDE companion extension. You must use a Seatbelt profile that allows network access.
- **In a Docker Container:** If you run Qwen Code inside a Docker (or Podman) container, the IDE integration can still connect to the VS Code extension running on your host machine. The CLI is configured to automatically find the IDE server on `host.docker.internal`. No special configuration is usually required, but you may need to ensure your Docker networking setup allows connections from the container to the host.

## Troubleshooting

If you encounter issues with IDE integration, here are some common error messages and how to resolve them.

### Connection Errors

- **Message:** `● Disconnected: Failed to connect to IDE companion extension for [IDE Name]. Please ensure the extension is running and try restarting your terminal. To install the extension, run /ide install.`
  - **Cause:** Qwen Code could not find the necessary environment variables (`QWEN_CODE_IDE_WORKSPACE_PATH` or `QWEN_CODE_IDE_SERVER_PORT`) to connect to the IDE. This usually means the IDE companion extension is not running or did not initialize correctly.
  - **Solution:**
    1.  Make sure you have installed the **Qwen Code Companion** extension in your IDE and that it is enabled.
    2.  Open a new terminal window in your IDE to ensure it picks up the correct environment.

- **Message:** `● Disconnected: IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`
  - **Cause:** The connection to the IDE companion was lost.
  - **Solution:** Run `/ide enable` to try and reconnect. If the issue continues, open a new terminal window or restart your IDE.

### Configuration Errors

- **Message:** `● Disconnected: Directory mismatch. Qwen Code is running in a different location than the open workspace in [IDE Name]. Please run the CLI from the same directory as your project's root folder.`
  - **Cause:** The CLI's current working directory is outside the folder or workspace you have open in your IDE.
  - **Solution:** `cd` into the same directory that is open in your IDE and restart the CLI.

- **Message:** `● Disconnected: To use this feature, please open a workspace folder in [IDE Name] and try again.`
  - **Cause:** You have no workspace open in your IDE.
  - **Solution:** Open a workspace in your IDE and restart the CLI.

### General Errors

- **Message:** `IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: [List of IDEs]`
  - **Cause:** You are running Qwen Code in a terminal or environment that is not a supported IDE.
  - **Solution:** Run Qwen Code from the integrated terminal of a supported IDE, like VS Code.

- **Message:** `No installer is available for IDE. Please install the Qwen Code Companion extension manually from the marketplace.`
  - **Cause:** You ran `/ide install`, but the CLI does not have an automated installer for your specific IDE.
  - **Solution:** Open your IDE's extension marketplace, search for "Qwen Code Companion", and install it manually.

---

### Users Reference Keyboard Shortcuts (`docs/users/reference/keyboard-shortcuts.md`)

# Qwen Code Keyboard Shortcuts

This document lists the available keyboard shortcuts in Qwen Code.

## General

| Shortcut                       | Description                                                                                                                                                                                                                                        |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Esc`                          | Close dialogs and suggestions. With an empty prompt, cancel an ongoing request; when idle outside IDE mode, press twice to open the rewind selector.                                                                                               |
| `Ctrl+C`                       | Cancel the ongoing request and clear the input. Press twice to exit the application.                                                                                                                                                               |
| `Ctrl+D`                       | Exit the application if the input is empty. Press twice to confirm.                                                                                                                                                                                |
| `Ctrl+L`                       | Clear the screen.                                                                                                                                                                                                                                  |
| `Ctrl+O` / `Alt/Option+T`      | Toggle expanded detail mode: expand or collapse all thinking blocks and tool outputs inline. Press again to collapse. When `ui.useTerminalBuffer` is off, toggling redraws the full conversation with untruncated output into terminal scrollback. |
| `Ctrl+S`                       | Stashes non-empty input for the current project and restores it on the next launch. With empty input, allows long responses to print fully, disabling truncation. Use your terminal's scrollback to view the entire output.                        |
| `Ctrl+T`                       | Toggle the display of tool descriptions.                                                                                                                                                                                                           |
| `Alt/Option+M`                 | Toggle Markdown output between rich rendered previews and raw/source mode. On macOS, the terminal must send Option as Meta.                                                                                                                        |
| `Shift+Tab` (`Tab` on Windows) | Cycle approval modes (`plan` → `default` → `auto-edit` → `auto` → `yolo`)                                                                                                                                                                          |

## Input Prompt

| Shortcut                                              | Description                                                                                                                                                                                                                                                                                                               |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `!`                                                   | Toggle shell mode when the input is empty.                                                                                                                                                                                                                                                                                |
| `?`                                                   | Toggle keyboard shortcuts display when the input is empty.                                                                                                                                                                                                                                                                |
| `/`                                                   | Open slash-command completion.                                                                                                                                                                                                                                                                                            |
| `@`                                                   | Open completion for files, folders, and other context.                                                                                                                                                                                                                                                                    |
| `Space` (empty prompt)                                | Start voice dictation when it and a voice model are configured; hold or tap behavior follows `general.voice.mode`.                                                                                                                                                                                                        |
| `Ctrl+Enter` / `Cmd+Enter` / `Shift+Enter` / `Ctrl+J` | Insert a newline.                                                                                                                                                                                                                                                                                                         |
| `Down Arrow`                                          | Row down, then snap to end, then history next.                                                                                                                                                                                                                                                                            |
| `Enter`                                               | Submit the current prompt. While a response is running, steer the current turn.                                                                                                                                                                                                                                           |
| `Ctrl+Q`                                              | Queue the current prompt or command for the next turn instead of steering; it runs after Qwen Code returns to idle.                                                                                                                                                                                                       |
| `Up Arrow` (at the top) / `Esc`                       | When queued messages are present, move them back into the input for editing (`Up Arrow` at the top whenever the input is shown; `Esc` only when the agent is idle). While the agent is responding and the input is empty, `Esc` cancels the ongoing request instead (queued messages are then moved back into the input). |
| `Meta+D` / `Meta+Delete` / `Ctrl+Delete`              | Delete the word to the right of the cursor.                                                                                                                                                                                                                                                                               |
| `Tab`                                                 | Autocomplete the current suggestion if one exists.                                                                                                                                                                                                                                                                        |
| `Up Arrow`                                            | Row up, then snap to start, then history prev.                                                                                                                                                                                                                                                                            |
| `Ctrl+A` / `Home`                                     | Move the cursor to the beginning of the line.                                                                                                                                                                                                                                                                             |
| `Ctrl+B` / `Left Arrow`                               | Move the cursor one character to the left. While the `@` completion menu shows category tabs, use `Ctrl+B` (the arrow switches tabs).                                                                                                                                                                                     |
| `Ctrl+C`                                              | Clear the input prompt                                                                                                                                                                                                                                                                                                    |
| `Esc` (double press)                                  | Clear the input prompt.                                                                                                                                                                                                                                                                                                   |
| `Ctrl+D` / `Delete`                                   | Delete the character to the right of the cursor.                                                                                                                                                                                                                                                                          |
| `Ctrl+E` / `End`                                      | Move the cursor to the end of the line.                                                                                                                                                                                                                                                                                   |
| `Ctrl+F` / `Right Arrow`                              | Move the cursor one character to the right. While the `@` completion menu shows category tabs, use `Ctrl+F` (the arrow switches tabs).                                                                                                                                                                                    |
| `Ctrl+H` / `Backspace`                                | Delete the character to the left of the cursor.                                                                                                                                                                                                                                                                           |
| `Ctrl+K`                                              | Delete from the cursor to the end of the line.                                                                                                                                                                                                                                                                            |
| `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B`      | Move the cursor one word to the left.                                                                                                                                                                                                                                                                                     |
| `Ctrl+N`                                              | Row down, then snap to end, then history next.                                                                                                                                                                                                                                                                            |
| `Ctrl+P`                                              | Row up, then snap to start, then history prev.                                                                                                                                                                                                                                                                            |
| `Ctrl+R`                                              | Reverse search through input/shell history.                                                                                                                                                                                                                                                                               |
| `Ctrl+Y`                                              | Retry the last failed request.                                                                                                                                                                                                                                                                                            |
| `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F`    | Move the cursor one word to the right.                                                                                                                                                                                                                                                                                    |
| `Ctrl+U`                                              | Delete from the cursor to the beginning of the line.                                                                                                                                                                                                                                                                      |
| `Ctrl+V` / `Option+V` (Windows: `Alt+V`)              | Paste clipboard content. If the clipboard contains an image, it will be saved and a reference to it will be inserted in the prompt.                                                                                                                                                                                       |
| `Ctrl+W` / `Meta+Backspace` / `Ctrl+Backspace`        | Delete the word to the left of the cursor.                                                                                                                                                                                                                                                                                |
| `Ctrl+X`                                              | Open the current input in an external editor.                                                                                                                                                                                                                                                                             |
| `Ctrl+Z`                                              | Undo the last input edit.                                                                                                                                                                                                                                                                                                 |
| `Ctrl+Shift+Z`                                        | Redo the last undone input edit.                                                                                                                                                                                                                                                                                          |

## Foreground Shell

These shortcuts apply while an interactive foreground shell command is running.

| Shortcut                            | Description                                                                                                                                                    |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Ctrl+F`                            | Toggle keyboard focus between the shell and the prompt. When no shell is running, `Ctrl+F` moves the prompt cursor right.                                      |
| `Ctrl+Shift+Up` / `Ctrl+Shift+Down` | Scroll the focused shell up or down.                                                                                                                           |
| `Ctrl+B`                            | Promote the shell to a background task. The child keeps running, the agent's turn unblocks, and the shell appears in `/tasks` and the Background tasks dialog. |

## Background tasks dialog

Focus the Background tasks pill in the footer (use `Down Arrow` from an empty composer — this moves through the live-agent panel and, if present, the Arena tab bar first) and press `Enter` to open the dialog. It lists background agents, shells, monitors, workflow runs, and memory dreams.

| Shortcut                  | Description                                                                                                                                              |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Up Arrow` / `Down Arrow` | Move the selection between tasks.                                                                                                                        |
| `Enter`                   | Open the detail view for the selected task.                                                                                                              |
| `x`                       | Stop the selected task (abandon a paused agent). A foreground agent that blocks your turn needs a second `x` to confirm.                                 |
| `r`                       | Resume the selected paused agent.                                                                                                                        |
| `p`                       | Cooperatively pause or resume the selected background workflow run. No new agents start while paused, but script code between agent calls keeps running. |
| `s`                       | Save the script of a finished (completed, failed, or cancelled) workflow run (detail view only).                                                         |
| `Left Arrow` / `Esc`      | Return to the list from the detail view, or close the dialog.                                                                                            |

## Suggestions

| Shortcut                | Description                                                                                                                         |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `Down Arrow` / `Ctrl+N` | Navigate down through the suggestions.                                                                                              |
| `Tab` / `Enter`         | Accept the selected suggestion.                                                                                                     |
| `Up Arrow` / `Ctrl+P`   | Navigate up through the suggestions.                                                                                                |
| `Right Arrow`           | Switch to the next completion category when category tabs are shown. Also accepts a ghost-text suggestion when the prompt is empty. |
| `Left Arrow`            | Switch to the previous completion category when category tabs are shown.                                                            |

> Note: while the `@` completion menu is showing category tabs, `Left Arrow` and
> `Right Arrow` switch categories instead of moving the cursor. Press `Esc` to
> dismiss the menu first if you need to move the cursor. `Alt/Option+Arrow` word
> movement is unaffected.

## History Search

Press `Ctrl+R` to search prompt history, or shell history while shell mode is active.

| Shortcut                     | Description                                                |
| ---------------------------- | ---------------------------------------------------------- |
| `Up Arrow` / `Down Arrow`    | Navigate through matching history entries.                 |
| `Left Arrow` / `Right Arrow` | Collapse or expand a long selected entry.                  |
| `Tab`                        | Accept the selected entry into the prompt without sending. |
| `Enter`                      | Submit the selected entry.                                 |
| `Esc`                        | Close history search.                                      |

## Radio Button Select

| Shortcut                      | Description                                                                                                   |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `Down Arrow` / `j` / `Ctrl+N` | Move selection down.                                                                                          |
| `Enter`                       | Confirm selection.                                                                                            |
| `Up Arrow` / `k` / `Ctrl+P`   | Move selection up.                                                                                            |
| `1-9`                         | Select an item by its number.                                                                                 |
| (multi-digit)                 | For items with numbers greater than 9, press the digits in quick succession to select the corresponding item. |

## History scrollback

Active when `ui.useTerminalBuffer` is enabled (Settings → UI → Virtualized History), screen reader mode is off, and Qwen Code is running in a compatible interactive terminal (`stdout` is a TTY, CI is inactive, and `TERM` is not `dumb`), which is the default for ordinary non-screen-reader sessions. In that mode conversation history is rendered inside an in-app viewport instead of the host terminal scrollback, so the keys below replace the terminal's native scroll.

| Shortcut        | Description                                                                     |
| --------------- | ------------------------------------------------------------------------------- |
| `Shift+Up`      | Scroll history up one line.                                                     |
| `Shift+Down`    | Scroll history down one line.                                                   |
| `PgUp`          | Scroll history up one page (viewport height).                                   |
| `PgDn`          | Scroll history down one page (viewport height).                                 |
| `Ctrl+Home`     | Jump to the top of the conversation.                                            |
| `Ctrl+End`      | Jump to the bottom (and re-engage live auto-follow).                            |
| **Mouse wheel** | Scroll history (3 lines per tick). Requires `ui.mouseTracking` (on by default). |

When `ui.useTerminalBuffer` is on and `ui.mouseTracking` is enabled (the default), the terminal forwards mouse events to qwen-code so the wheel can drive the in-app viewport. As a side effect, native click-and-drag text selection is consumed by the program, so qwen-code provides its own: **drag to select text in the history viewport, double-click to select a word, triple-click to select a line.** The selection is highlighted and copied to the clipboard when you release the mouse (works locally, over SSH via OSC 52, and inside tmux). A single click clears the selection; scrolling or new output clears it too. Selection is limited to the visible viewport for now. You can still fall back to the terminal's own selection by holding `Shift` (or `Option` on macOS Terminal / iTerm) while dragging.

A **plain single click** on an http(s) OSC 8 hyperlink in the viewport opens it in your default browser. Non-http(s) links (for example `mailto:`) are copied to the clipboard instead. **Right-click** on a link or an active text selection opens an in-app context menu with **Open Link**, **Copy Link Address**, or **Copy Selection**. The menu can be navigated with the arrow keys, executed with `Enter`, and dismissed with `Esc` or by clicking outside it. Set `ui.mouseTracking` to `false` to stop qwen-code from capturing the mouse entirely; that restores the terminal's native right-click menu, OSC 8 hyperlink clicks, and click-and-drag selection, but the in-app viewport no longer responds to the mouse, so use the keyboard shortcuts above to scroll.

### tmux trackpad scrolling

Inside tmux, some terminals translate trackpad or wheel gestures into plain `Up Arrow` and `Down Arrow` sequences before qwen-code sees them. Those bytes are identical to real arrow-key presses, so qwen-code cannot tell whether you meant to scroll the viewport or navigate prompt history.

If trackpad scrolling changes the prompt history in tmux, make sure `ui.useTerminalBuffer` is enabled; then use `Shift+Up` / `Shift+Down`, or the mouse wheel when tmux forwards wheel events to the app (requires `ui.mouseTracking`). If you would rather have plain `Up Arrow` / `Down Arrow` scroll the viewport while the composer is empty, set `ui.arrowKeysScrollTranscript` to `true` — input history then moves to `Ctrl+P` / `Ctrl+N` from an empty composer. If you prefer host scrollback, adjust your tmux mouse bindings for wheel events.

## IDE Integration

| Shortcut | Description                       |
| -------- | --------------------------------- |
| `Ctrl+G` | See context CLI received from IDE |

---

### Users Support Tos Privacy (`docs/users/support/tos-privacy.md`)

# Qwen Code: Terms of Service and Privacy Notice

Qwen Code is an open-source AI coding assistant tool maintained by the Qwen Code team. This document outlines the terms of service and privacy policies that apply when using Qwen Code's authentication methods and AI model services.

## How to determine your authentication method

Qwen Code supports four authentication methods to access AI models. Your authentication method determines which terms of service and privacy policies apply to your usage:

1. **Qwen OAuth** — Log in with your qwen.ai account (free tier discontinued 2026-04-15)
2. **Alibaba Cloud Coding Plan** — Use an API key from Alibaba Cloud
3. **API Key** — Bring your own API key
4. **Vertex AI** — Use Google Cloud Vertex AI

For each authentication method, different Terms of Service and Privacy Notices may apply depending on the underlying service provider.

| Authentication Method     | Provider          | Terms of Service                                                   | Privacy Notice                                                     |
| :------------------------ | :---------------- | :----------------------------------------------------------------- | :----------------------------------------------------------------- |
| Qwen OAuth                | Qwen AI           | [Qwen Terms of Service](https://qwen.ai/termsservice)              | [Qwen Privacy Policy](https://qwen.ai/privacypolicy)               |
| Alibaba Cloud Coding Plan | Alibaba Cloud     | See [details below](#2-if-you-are-using-alibaba-cloud-coding-plan) | See [details below](#2-if-you-are-using-alibaba-cloud-coding-plan) |
| API Key                   | Various Providers | Depends on your chosen API provider (OpenAI, Anthropic, etc.)      | Depends on your chosen API provider                                |
| Vertex AI                 | Google Cloud      | [Google Cloud Terms](https://cloud.google.com/terms)               | [Google Cloud Privacy](https://cloud.google.com/privacy)           |

## 1. If you are using Qwen OAuth Authentication

When you authenticate using your qwen.ai account, these Terms of Service and Privacy Notice documents apply:

- **Terms of Service:** Your use is governed by the [Qwen Terms of Service](https://qwen.ai/termsservice).
- **Privacy Notice:** The collection and use of your data is described in the [Qwen Privacy Policy](https://qwen.ai/privacypolicy).

For details about authentication setup, quotas, and supported features, see [Authentication Setup](../configuration/settings).

## 2. If you are using Alibaba Cloud Coding Plan

When you authenticate using an API key from Alibaba Cloud, the applicable Terms of Service and Privacy Notice from Alibaba Cloud apply.

Alibaba Cloud Coding Plan is available in two regions:

- **阿里云百炼 (aliyun.com)** — [bailian.console.aliyun.com](https://bailian.console.aliyun.com)
- **Alibaba Cloud (alibabacloud.com)** — [bailian.console.alibabacloud.com](https://bailian.console.alibabacloud.com)

> [!important]
>
> When using Alibaba Cloud Coding Plan, you are subject to Alibaba Cloud's terms and privacy policies. Please review their documentation for specific details about data usage, retention, and privacy practices.

## 3. If you are using your own API Key

When you authenticate using API keys from other providers, the applicable Terms of Service and Privacy Notice depend on your chosen provider.

> [!important]
>
> When using your own API key, you are subject to the terms and privacy policies of your chosen API provider, not Qwen Code's terms. Please review your provider's documentation for specific details about data usage, retention, and privacy practices.

Qwen Code supports various OpenAI-compatible providers. Please refer to your specific provider's terms of service and privacy policy for detailed information.

## 4. If you are using Vertex AI

When you authenticate with Google Cloud Vertex AI, the applicable Terms of Service and Privacy Notice are Google Cloud's.

> [!important]
>
> When using Vertex AI, you are subject to [Google Cloud's Terms of Service](https://cloud.google.com/terms) and [Google Cloud Privacy Notice](https://cloud.google.com/privacy), not Qwen Code's terms. Please review Google Cloud's documentation for specific details about data usage, retention, and privacy practices.

## Chrome extension and Browser Use

The Qwen Code Chrome extension connects Chrome to Qwen Code running on your computer. Its side panel displays the local Qwen Code web application, and Browser Use exchanges browser commands and results through a local Native Messaging host. The following describes data handled for browser tasks, separately from the optional usage statistics described below.

### Browser data used for your tasks

Browser tools can access open HTTP(S) tab titles and URLs, page text and structure, screenshots, browser interaction results, and debugging information such as console messages, network activity, and cookies. Explicit browser-history searches return matching URLs, page titles, and visit times within the requested query limits. Depending on the pages and tasks you choose, these results may contain personal identifiers, health information, financial or payment information, authentication information, personal communications, and location information. The extension also uses navigation-source information to associate new pages opened by a recent assistant action with the same browser session.

These capabilities support the browser tasks and web development work you request from Qwen Code. Browser tools operate in your Chrome profile, including pages where you are signed in. Choose the pages and tasks you share with the assistant accordingly.

### Local processing and AI providers

The extension sends browser commands and results to Qwen Code on the same computer. Qwen Code may include those results in conversation context and transmit them to the AI provider configured for that session. The provider's privacy, retention, and model-training terms apply as described elsewhere in this notice. The extension's local connection is one part of this data flow; subsequent processing may take place at your chosen AI provider.

### Stored data and user controls

The extension stores connection preferences, an optional local daemon authentication token, and a persistent browser-instance identifier in Chrome extension local storage. It also stores tab and session ownership state in Chrome session storage to support cleanup after its background service worker restarts.

Browser results included in Qwen Code conversations or saved by browser tools may remain in local conversation records, screenshots, downloads, or other output files. Manage these records using the applicable Qwen Code and filesystem controls. AI-provider retention is governed separately by the selected provider.

You can disable or uninstall the Chrome extension in Chrome's extension manager to stop its browser integration. Clearing extension storage removes its saved preferences, token, and instance identifier. Removing the extension leaves the separately installed Qwen Code application, its Native Messaging host, local conversations and files, and copies already sent to an AI provider to be managed separately.

### Limited use of browser data

Qwen Code's use and transfer of data received through the Chrome extension adhere to the [Chrome Web Store User Data Policy](https://developer.chrome.com/docs/webstore/program-policies/user-data-faq), including its Limited Use requirements. Browser data is used to provide the extension's single purpose: connecting Chrome to Qwen Code for user-requested browser assistance. It is not sold, used for advertising, or used to determine creditworthiness or lending eligibility. Transfers are limited to providing that functionality, including processing by the AI provider you configure, and other uses permitted by that policy.

For questions about browser data handling, contact the team through the [Qwen Code issue tracker](https://github.com/QwenLM/qwen-code/issues). Share only the details needed to explain the question; remove credentials and private page content from public reports.

## Usage Statistics and Telemetry

Qwen Code may collect anonymous usage statistics and [telemetry](../../developers/development/telemetry) data to improve the user experience and product quality. This data collection is optional and can be controlled through configuration settings.

### What Data is Collected

When enabled, Qwen Code may collect:

- Anonymous usage statistics (commands run, performance metrics)
- Error reports and crash data
- Feature usage patterns

### Data Collection by Authentication Method

- **Qwen OAuth:** Usage statistics are governed by Qwen's privacy policy. You can opt-out through Qwen Code's configuration settings.
- **Alibaba Cloud Coding Plan:** Usage statistics are governed by Alibaba Cloud's privacy policy. You can opt-out through Qwen Code's configuration settings.
- **API Key:** No additional data is collected by Qwen Code beyond what your chosen API provider collects.
- **Vertex AI:** Usage statistics are governed by Google Cloud's privacy policy. No additional data is collected by Qwen Code beyond what Google Cloud collects.

## Frequently Asked Questions (FAQ)

### 1. Is my code, including prompts and answers, used to train AI models?

Whether your code, including prompts and answers, is used to train AI models depends on your authentication method and the specific AI service provider you use:

- **Qwen OAuth**: Data usage is governed by [Qwen's Privacy Policy](https://qwen.ai/privacypolicy). Please refer to their policy for specific details about data collection and model training practices.

- **Alibaba Cloud Coding Plan**: Data usage is governed by Alibaba Cloud's privacy policy. Please refer to their policy for specific details about data collection and model training practices.

- **API Key**: Data usage depends entirely on your chosen API provider. Each provider has their own data usage policies. Please review the privacy policy and terms of service of your specific provider.

- **Vertex AI**: Data usage is governed by [Google Cloud's Terms of Service](https://cloud.google.com/terms) and [Privacy Notice](https://cloud.google.com/privacy). Please review Google Cloud's policies for specific details about data collection and model training practices.

**Important**: Qwen Code itself does not use your prompts, code, or responses for model training. Any data usage for training purposes would be governed by the policies of the AI service provider you authenticate with.

### 2. What are Usage Statistics and what does the opt-out control?

The **Usage Statistics** setting controls optional data collection by Qwen Code for improving the user experience and product quality.

When enabled, Qwen Code may collect:

- Anonymous telemetry (commands run, performance metrics, feature usage)
- Error reports and crash data
- General usage patterns

**What is NOT collected as usage statistics:**

- Your code content
- Prompts sent to AI models
- Responses from AI models
- Personal information

The Usage Statistics setting only controls data collection by Qwen Code itself. It does not affect what data your chosen AI service provider (Qwen, OpenAI, etc.) may collect according to their own privacy policies.

### 3. How do I switch between authentication methods?

You can switch between Qwen OAuth, Alibaba Cloud Coding Plan, your own API key, and Vertex AI at any time:

1. **During startup**: Choose your preferred authentication method when prompted
2. **Within the CLI**: Use the `/auth` command to reconfigure your authentication method
3. **Environment variables**: Set up `.env` files for automatic API key authentication

For detailed instructions, see the [Authentication Setup](../configuration/auth.md) documentation.

---

### Users Support Troubleshooting (`docs/users/support/troubleshooting.md`)

# Troubleshooting

This guide provides solutions to common issues and debugging tips, including topics on:

- Authentication or login errors
- Frequently asked questions (FAQs)
- Debugging tips
- Existing GitHub Issues similar to yours or creating new Issues

## Authentication or login errors

- **Error: `Qwen OAuth free tier was discontinued on 2026-04-15`**
  - **Cause:** Qwen OAuth is no longer available as of April 15, 2026.
  - **Solution:** Switch to a different authentication method. Run `qwen` → `/auth` and choose one of:
    - **API Key**: Use an API key from Alibaba Cloud Model Studio ([Beijing](https://bailian.console.aliyun.com/) / [intl](https://modelstudio.console.alibabacloud.com/)). See the API setup guide ([Beijing](https://bailian.console.aliyun.com/cn-beijing/?tab=doc#/doc/?type=model&url=3023091) / [intl](https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=doc#/doc/?type=model&url=2974721)).
    - **Alibaba Cloud Coding Plan**: Subscribe for a fixed monthly fee with higher quotas. See the Coding Plan guide ([Beijing](https://bailian.console.aliyun.com/cn-beijing/?tab=coding-plan#/efm/coding-plan-index) / [intl](https://modelstudio.console.alibabacloud.com/?tab=coding-plan#/efm/coding-plan-index)).

- **Error: `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`, `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, or `unable to get local issuer certificate`**
  - **Cause:** You may be on a corporate network with a firewall that intercepts and inspects SSL/TLS traffic. This often requires a custom root CA certificate to be trusted by Node.js.
  - **Solution:** Set the `NODE_EXTRA_CA_CERTS` environment variable to the absolute path of your corporate root CA certificate file.
    - Example: `export NODE_EXTRA_CA_CERTS=/path/to/your/corporate-ca.crt`

- **Error: `Connection error. (cause: fetch failed)` against a self-signed endpoint**
  - **Cause:** You are pointing Qwen Code at a self-hosted server (for example a local model behind `https://`) whose TLS certificate is self-signed, so Node.js rejects it.
  - **Solution:** Prefer trusting the certificate via `NODE_EXTRA_CA_CERTS` (above). If that is not practical in a trusted lab/private network, skip verification with the `--insecure` flag (or `QWEN_TLS_INSECURE=1`):
    - Example: `qwen --insecure --openaiBaseUrl https://192.168.1.10:8080 ...`
    - **Warning:** Disabling verification removes protection against man-in-the-middle attacks. Only use it for endpoints you fully trust.

- **Error: `Device authorization flow failed: fetch failed`**
  - **Cause:** Node.js could not reach Qwen OAuth endpoints (often a proxy or SSL/TLS trust issue). When available, Qwen Code will also print the underlying error cause (for example: `UNABLE_TO_VERIFY_LEAF_SIGNATURE`). Note: this error is specific to the legacy Qwen OAuth flow.
  - **Solution:**
    - If you are still using Qwen OAuth, switch to API Key or Coding Plan via `/auth`.
    - If you are behind a proxy, set it via `qwen --proxy <url>` (or the `proxy` setting in `settings.json`).
    - If your network uses a corporate TLS inspection CA, set `NODE_EXTRA_CA_CERTS` as described above.

- **Issue: Unable to display UI after authentication failure**
  - **Cause:** If authentication fails after selecting an authentication type, the `security.auth.selectedType` setting may be persisted in `settings.json`. On restart, the CLI may get stuck trying to authenticate with the failed auth type and fail to display the UI.
  - **Solution:** Clear the `security.auth.selectedType` configuration item in your `settings.json` file:
    - Open `~/.qwen/settings.json` (or `./.qwen/settings.json` for project-specific settings)
    - Remove the `security.auth.selectedType` field
    - Restart the CLI to allow it to prompt for authentication again

## Frequently asked questions (FAQs)

- **Q: How do I update Qwen Code to the latest version?**
  - A: If you installed Qwen Code with the standalone installer, rerun the standalone install command. If you installed it globally via `npm`, update it using the command `npm install @qwen-code/qwen-code@latest`. If you compiled it from source, pull the latest changes from the repository, and then rebuild using the command `npm run build`.

- **Q: Where are the Qwen Code configuration or settings files stored?**
  - A: The Qwen Code configuration is stored in two `settings.json` files:
    1. In your home directory: `~/.qwen/settings.json`.
    2. In your project's root directory: `./.qwen/settings.json`.

    Refer to [Qwen Code Configuration](../configuration/settings) for more details.

- **Q: Why don't I see cached token counts in my stats output?**
  - A: Cached token information is only displayed when cached tokens are being used. This feature is available for API key users (e.g., Alibaba Cloud Model Studio API key or Google Cloud Vertex AI). You can still view your total token usage using the `/stats` command.

- **Q: A customization (extension, hook, skill, MCP server, or subagent) seems to be breaking Qwen Code. How do I isolate it?**
  - A: Start Qwen Code with the `--safe-mode` flag to disable all customizations — context files, hooks, extensions, skills, MCP servers, custom subagents (only built-in subagents load), permission rules, settings-sourced approval mode overrides, memory features, cross-session messaging, and sandbox settings — for the session. Note: the CLI flags `--yolo` and `--approval-mode` still take effect in safe mode. If the problem disappears in safe mode, re-enable your customizations one at a time to find the culprit.
    - Example: `qwen --safe-mode`
    - Alternative: set the environment variable `QWEN_CODE_SAFE_MODE=true` if the CLI cannot accept flags.
    - Note: unlike the flag, an exported `QWEN_CODE_SAFE_MODE` is inherited by child processes. A `qwen serve` daemon or an editor started from that shell hosts every one of its sessions in safe mode, so those sessions are suppressed too: they write no session record, and other sessions can neither list nor message them. Unset the variable for the daemon rather than leaving it exported in the shell that starts it.
    - Note: "MCP servers" here means servers configured in `settings.json` / project `.mcp.json` — local, ambient state that safe mode is meant to isolate against. MCP servers you explicitly supply for the current invocation (an embedding ACP client's `session/new` `mcpServers`, or `--mcp-config`) are not local/ambient state and are still honored under safe mode.

## Attachment upload returns HTTP 413 behind a reverse proxy

A proxy may reject a request before it reaches `qwen serve`, even when the attachment is within the daemon's 8 MiB file limit. Raising only a daemon body limit does not change the proxy limit.

Use a daemon and TypeScript SDK/Web Shell build that supports `session_attachment_chunk_upload`. Attachments larger than 512 KiB are then uploaded in 512 KiB requests without changing their bytes or image quality. Older daemons keep the single-request upload path. If you operate the proxy, you can also raise its request-body limit to accommodate the original attachment request. Proxies with limits below 512 KiB still need a configuration change.

The attachment limit remains 8 MiB. If an upload expires or the daemon restarts, retry the attachment from the UI. A completed upload whose response was lost can remain in the session's attachments; cancellation does not remove a file that has already completed.

## Common error messages and solutions

- **Error: `EADDRINUSE` (Address already in use) when starting an MCP server.**
  - **Cause:** Another process is already using the port that the MCP server is trying to bind to.
  - **Solution:**
    Either stop the other process that is using the port or configure the MCP server to use a different port.

- **Error: Command not found (when attempting to run Qwen Code with `qwen`).**
  - **Cause:** The CLI is not correctly installed or it is not in your system's `PATH`.
  - **Solution:**
    The update depends on how you installed Qwen Code:
    - If you installed `qwen` with the standalone installer, rerun the standalone install command and then open a new terminal.
    - If you installed `qwen` globally, check that your `npm` global binary directory is in your `PATH`. You can update using the command `npm install @qwen-code/qwen-code@latest`.
    - If you are running `qwen` from source, ensure you are using the correct command to invoke it (e.g. `node packages/cli/dist/index.js ...`). To update, pull the latest changes from the repository, and then rebuild using the command `npm run build`.

- **Error: `MODULE_NOT_FOUND` or import errors.**
  - **Cause:** Dependencies are not installed correctly, or the project hasn't been built.
  - **Solution:**
    1.  Run `npm install` to ensure all dependencies are present.
    2.  Run `npm run build` to compile the project.
    3.  Verify that the build completed successfully with `npm run start`.

- **Error: "Operation not permitted", "Permission denied", or similar.**
  - **Cause:** When sandboxing is enabled, Qwen Code may attempt operations that are restricted by your sandbox configuration, such as writing outside the project directory or system temp directory.
  - **Solution:** Refer to the [Configuration: Sandboxing](../features/sandbox) documentation for more information, including how to customize your sandbox configuration.

- **Qwen Code is not running in interactive mode in "CI" environments**
  - **Issue:** Qwen Code does not enter interactive mode (no prompt appears) if an environment variable starting with `CI_` (e.g. `CI_TOKEN`) is set. This is because the `is-in-ci` package, used by the underlying UI framework, detects these variables and assumes a non-interactive CI environment.
  - **Cause:** The `is-in-ci` package checks for the presence of `CI`, `CONTINUOUS_INTEGRATION`, or any environment variable with a `CI_` prefix. When any of these are found, it signals that the environment is non-interactive, which prevents the CLI from starting in its interactive mode.
  - **Solution:** If the `CI_` prefixed variable is not needed for the CLI to function, you can temporarily unset it for the command. e.g. `env -u CI_TOKEN qwen`

- **DEBUG mode not working from project .env file**
  - **Issue:** Setting `DEBUG=true` in a project's `.env` file doesn't enable debug mode for the CLI.
  - **Cause:** The `DEBUG` and `DEBUG_MODE` variables are automatically excluded from project `.env` files to prevent interference with the CLI behavior.
  - **Solution:** Use a `.qwen/.env` file instead, or configure the `advanced.excludedEnvVars` setting in your `settings.json` to exclude fewer variables.

- **Trackpad scrolling in tmux changes prompt history instead of scrolling the conversation**
  - **Issue:** In a tmux session, trackpad or wheel scrolling may cycle through previous prompts, similar to pressing `Up Arrow` or `Down Arrow`.
  - **Cause:** tmux can translate wheel gestures into plain arrow-key sequences. Those sequences are indistinguishable from real arrow-key presses by the time qwen-code receives them.
  - **Solution:** If screen reader mode is disabled, make sure `ui.useTerminalBuffer` is enabled; then use `Shift+Up` / `Shift+Down`, or the mouse wheel when tmux forwards wheel events to the app (requires `ui.mouseTracking`). To hand plain `Up Arrow` / `Down Arrow` to the viewport while the composer is empty, set `ui.arrowKeysScrollTranscript` to `true`; input history is then reachable with `Ctrl+P` / `Ctrl+N`. If you prefer host scrollback, adjust your tmux mouse bindings for wheel events.

- **Right-click does nothing, links do not open, or text cannot be selected in the terminal**
  - **Issue:** Native right-click context menus, OSC 8 hyperlink clicks, and terminal-native text selection stop working while Qwen Code is running.
  - **Cause:** When `ui.mouseTracking` is enabled (the default), Qwen Code captures all mouse events via SGR mouse tracking to power in-app text selection, click-to-position, row hover, history-item toggling, and viewport scrolling. The terminal forwards every mouse event to the app instead of handling it natively. Qwen Code supplies its own replacements while tracking is on: a single click opens an http(s) OSC 8 hyperlink, and right-click on a link or text selection opens an in-app context menu.
  - **Solution:** If you prefer the terminal's native handling, set `"ui.mouseTracking": false` in your `settings.json`. This turns off all in-app mouse interaction, including the in-app link open and context menu. In Virtualized History (`ui.useTerminalBuffer: true`, the default), the wheel will no longer scroll the transcript — use `Shift+↑/↓`, `PgUp/PgDn`, or `Ctrl+Home/End` instead. To also restore native terminal scrollback, set `"ui.useTerminalBuffer": false`. Requires restart.

## IDE Companion not connecting

- Ensure VS Code has a single workspace folder open.
- Restart the integrated terminal after installing the extension so it inherits:
  - `QWEN_CODE_IDE_WORKSPACE_PATH`
  - `QWEN_CODE_IDE_SERVER_PORT`
- If running in a container, verify `host.docker.internal` resolves. Otherwise, map the host appropriately.
- Reinstall the companion with `/ide install` and use “Qwen Code: Run” in the Command Palette to verify it launches.

## Connection resets on some networks (TLS-stack-selective middleboxes)

- **Issue:** On some carrier or enterprise networks, HTTPS connections to API endpoints are reset (TCP RST) immediately after the TLS ClientHello. Qwen Code keeps retrying (`ECONNRESET` is a retryable transport error) until the budget is exhausted, then surfaces a generic connection error — while `curl`, Python, and other tools work fine on the same machine and network at the same moment.
- **Cause:** Some middleboxes filter connections by TLS ClientHello fingerprint and reset those emitted by particular TLS stack generations. On affected links, OpenSSL 3.0-era stacks and Electron/BoringSSL (used by the VS Code extension and the daemon) are reset, while OpenSSL 3.5-generation stacks — including current Node.js builds — connect normally.
- **Solution:** Make the connection with a 3.5-generation TLS stack:
  - **CLI:** use an up-to-date build of a supported Node.js line (current Node 22/24/25 builds all ship a 3.5-generation OpenSSL). Older builds of the same major may still be on an affected generation — update rather than pin to an old minor.
  - **VS Code extension / daemon:** the extension runs on Electron's TLS stack and cannot change it client-side. Run a small local relay — a local process on a 3.5-generation stack — and point Qwen Code at it via the model base URL override (the custom endpoint in `settings.json`, or `--openaiBaseUrl` for the CLI), so the TLS egress happens with the newer stack.
- **How to distinguish this from a flaky network:**
  - the reset is deterministic — every attempt fails immediately, across retries, and does not recover over time;
  - on the same machine and moment, other TLS stacks succeed against the same endpoint;
  - pinning TLS versions or changing DNS resolution does not help.

## Exit Codes

The Qwen Code uses specific exit codes to indicate the reason for termination. This is especially useful for scripting and automation.

| Exit Code | Error Type                 | Description                                                                                         |
| --------- | -------------------------- | --------------------------------------------------------------------------------------------------- |
| 41        | `FatalAuthenticationError` | An error occurred during the authentication process.                                                |
| 42        | `FatalInputError`          | Invalid or missing input was provided to the CLI. (non-interactive mode only)                       |
| 44        | `FatalSandboxError`        | An error occurred with the sandboxing environment (e.g. Docker, Podman, or Seatbelt).               |
| 52        | `FatalConfigError`         | A configuration file (`settings.json`) is invalid or contains errors.                               |
| 53        | `FatalTurnLimitedError`    | The maximum number of conversational turns for the session was reached. (non-interactive mode only) |

## Debugging Tips

- **CLI debugging:**
  - Use the `--verbose` flag (if available) with CLI commands for more detailed output.
  - Check the CLI logs, often found in a user-specific configuration or cache directory.

- **Core debugging:**
  - Check the server console output for error messages or stack traces.
  - Increase log verbosity if configurable.
  - Use Node.js debugging tools (e.g. `node --inspect`) if you need to step through server-side code.

- **Tool issues:**
  - If a specific tool is failing, try to isolate the issue by running the simplest possible version of the command or operation the tool performs.
  - For `run_shell_command`, check that the command works directly in your shell first.
  - For _file system tools_, verify that paths are correct and check the permissions.

- **Pre-flight checks:**
  - Always run `npm run preflight` before committing code. This can catch many common issues related to formatting, linting, and type errors.

## Existing GitHub Issues similar to yours or creating new Issues

If you encounter an issue that was not covered here in this _Troubleshooting guide_, consider searching the Qwen Code [Issue tracker on GitHub](https://github.com/QwenLM/qwen-code/issues). If you can't find an issue similar to yours, consider creating a new GitHub Issue with a detailed description. Pull requests are also welcome!

---

### Users Support Uninstall (`docs/users/support/Uninstall.md`)

# Uninstall

Your uninstall method depends on how you installed the CLI.

## Method 1: Using npx

npx runs packages from a temporary cache without a permanent installation. To "uninstall" the CLI, you must clear this cache, which will remove qwen-code and any other packages previously executed with npx.

The npx cache is a directory named `_npx` inside your main npm cache folder. You can find your npm cache path by running `npm config get cache`.

**For macOS / Linux**

```bash
# The path is typically ~/.npm/_npx
rm -rf "$(npm config get cache)/_npx"
```

**For Windows**

_Command Prompt_

```cmd
:: The path is typically %LocalAppData%\npm-cache\_npx
rmdir /s /q "%LocalAppData%\npm-cache\_npx"
```

_PowerShell_

```powershell
# The path is typically $env:LocalAppData\npm-cache\_npx
Remove-Item -Path (Join-Path $env:LocalAppData "npm-cache\_npx") -Recurse -Force
```

## Method 2: Using npm (Global Install)

If you installed the CLI globally (e.g. `npm install @qwen-code/qwen-code`), use the `npm uninstall` command with the `-g` flag to remove it.

```bash
npm uninstall -g @qwen-code/qwen-code
```

This command completely removes the package from your system.

## Method 3: Standalone Install

If you installed via the standalone installer (`curl ... | bash` or `irm ... | iex`), use the dedicated uninstall script.

**Linux / macOS**

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

**Windows**

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

The uninstaller removes the standalone runtime, generated `qwen` wrapper, and installer-managed PATH changes. Your Qwen Code configuration (`~/.qwen`) is preserved by default.

---

### Users Features Channels Dingtalk (`docs/users/features/channels/dingtalk.md`)

# DingTalk (Dingtalk)

This guide covers setting up a Qwen Code channel on DingTalk (钉钉).

## Prerequisites

- A DingTalk organization account
- A DingTalk bot application with AppKey and AppSecret (see below)

## Creating a Bot

1. Go to the [DingTalk Developer Portal](https://open-dev.dingtalk.com)
2. Create a new application (or use an existing one)
3. Under the application, enable the **Robot** capability
4. In Robot settings, enable **Stream Mode** (机器人协议 → Stream 模式)
5. Note the **AppKey** (Client ID) and **AppSecret** (Client Secret) from the application credentials page

### Stream Mode

DingTalk Stream mode uses an outbound WebSocket connection — no public URL or server is needed. The bot connects to DingTalk's servers, which push messages through the WebSocket. This is the simplest deployment model.

## Configuration

Add the channel to `~/.qwen/settings.json`:

```json
{
  "channels": {
    "my-dingtalk": {
      "type": "dingtalk",
      "clientId": "$DINGTALK_CLIENT_ID",
      "clientSecret": "$DINGTALK_CLIENT_SECRET",
      "useConnectionManager": true,
      "privatePolicy": "open",
      "sessionScope": "user",
      "cwd": "/path/to/your/project",
      "instructions": "You are a concise coding assistant responding via DingTalk.",
      "groupPolicy": "open",
      "atSender": true,
      "groups": {
        "*": { "requireMention": true }
      }
    }
  }
}
```

Set the credentials as environment variables:

```bash
export DINGTALK_CLIENT_ID=<your-app-key>
export DINGTALK_CLIENT_SECRET=<your-app-secret>
```

Or define them in the `env` section of `settings.json`:

```json
{
  "env": {
    "DINGTALK_CLIENT_ID": "your-app-key",
    "DINGTALK_CLIENT_SECRET": "your-app-secret"
  }
}
```

### Interactive Cards

Add an `interactiveCards` object to opt in to DingTalk status and question
cards. Omitting the object disables interactive cards. When the object is
present, the overall switch and both card types default to enabled, and
question cards time out after 270,000 milliseconds (270 seconds).

```json
{
  "channels": {
    "my-dingtalk": {
      "type": "dingtalk",
      "clientId": "$DINGTALK_CLIENT_ID",
      "clientSecret": "$DINGTALK_CLIENT_SECRET",
      "interactiveCards": {
        "enabled": true,
        "statusCard": { "enabled": true },
        "questionCard": {
          "enabled": true,
          "timeoutMs": 270000
        }
      }
    }
  }
}
```

Set `interactiveCards.enabled` to `false` to disable all interactive cards.
Use `statusCard.enabled` or `questionCard.enabled` to disable one card type,
and set `questionCard.timeoutMs` to a finite positive number to change how long
Qwen Code waits for a question-card response. Values above 2,147,483,647
milliseconds (about 24.8 days) are capped at that maximum. Interactive cards
are configured through `settings.json` or the management API; the Web Shell
channel editor does not render them, and it preserves the stored object when
you edit other fields.

### Connection Recovery

`useConnectionManager` defaults to `true`. The connection manager monitors the Stream WebSocket and replaces the DingTalk SDK client when the connection stops responding. You should normally leave it enabled.

Set `"useConnectionManager": false` to disable Qwen Code's connection manager and fall back to the SDK's keepalive and automatic reconnect behavior.

### Turn Output Mode

The [shared `outputMode` setting](./overview#turn-output-mode) controls when DingTalk delivers assistant results. DingTalk is currently the only adapter integrated with this policy. The default is `per_turn`, including when `outputMode` is omitted:

- `per_task`: wait for the main task and its associated background tasks and notifications to finish, then deliver one final result card containing the task's last non-empty assistant reply.
- `per_response`: each complete assistant response gets its own completed result card. Token chunks update the current card; they do not create new cards. Background assistant responses are delivered separately too.
- `per_turn`: the main status card completes with the turn's last non-empty assistant reply as soon as the main prompt ends. Each later background notification turn keeps its own last non-empty assistant reply and sends it as a separate completed card.

Background shell, monitor, and workflow output in `per_turn` and `per_task` includes a heading with its kind, status, and task label when available. In `per_response`, the response body is delivered as-is. Background agent replies keep their original body in every mode.

In the default `per_turn` mode, background tasks never extend the main card's lifetime, and a later callback cannot overwrite it. For example, a main result followed by eleven separate background notification turns produces a main result card and eleven follow-up result cards. Choose `per_task` to wait for that task's associated background work and receive one final result instead. The result comes from the assistant; no additional summary is generated and intermediate replies are not concatenated.

```json
{
  "channels": {
    "my-dingtalk": {
      "type": "dingtalk",
      "clientId": "$DINGTALK_CLIENT_ID",
      "clientSecret": "$DINGTALK_CLIENT_SECRET",
      "outputMode": "per_turn",
      "interactiveCards": {
        "enabled": true,
        "statusCard": { "enabled": true }
      }
    }
  }
}
```

Interactive status cards provide the native card presentation. When status cards are unavailable or all interactive cards are disabled, the same output policy applies through ordinary messages: `per_task` waits for the complete task, `per_response` sends each complete response, and `per_turn` sends one result per turn. Background results that exceed the card content limit also fall back to ordinary messages. Platform message-length limits may split long text. File and image delivery keeps its existing rules.

The setting applies to DingTalk conversation replies and their associated background follow-ups. It does not merge unrelated tasks in the same conversation. Channel loops and webhook runs retain their existing presentation.

`per_task` waits for Agent, background shell, monitor, and workflow work linked to that prompt, including work started by their notification turns. Paused tasks and long-running monitors keep it open until they finish or you cancel. Future scheduled runs and independently managed daemon child sessions are separate work and are not included in this task boundary.

If Todo Stop Guard yields to a message queued in the same session, the waiting `per_task` request ends as cancelled so the queued message can start. Its retained reply is not delivered as a successful task result. Associated background work remains available to the session.

Standalone background turns that are interrupted, or have not ended after ten minutes, may deliver a result marked as partial. This does not reopen a completed main card.

Only `per_task`, `per_response`, and `per_turn` are accepted. The unpublished `final_only` and `process_and_result` values are not aliases; replace them with the desired mode. Removing `outputMode` restores the `per_turn` default. There is no separate background-aggregation toggle.

## Running

```bash
# Start only the DingTalk channel
qwen channel start my-dingtalk

# Or start all configured channels together
qwen channel start
```

Open DingTalk and send a message to the bot. You should see a 👀 emoji reaction appear while the agent processes, followed by the response.

## Daemon Webhook Delivery

When the channel runs under `qwen serve`, authenticated external Webhook events can trigger unattended agent tasks and deliver the final Markdown response to either a DingTalk user or group. Use the existing Webhook target fields; no separate channel type is required:

```json
{
  "webhooks": {
    "sources": {
      "manual-test": {
        "secretEnv": "QWEN_CHANNEL_DINGTALK_TEST_SECRET",
        "targets": {
          "operator": {
            "chatId": "DINGTALK_USER_ID",
            "senderId": "webhook:manual-test",
            "isGroup": false
          },
          "team": {
            "chatId": "OPEN_CONVERSATION_ID",
            "senderId": "webhook:manual-test",
            "isGroup": true
          }
        }
      }
    }
  }
}
```

Every target must set `isGroup` explicitly. For a direct message, `chatId` is the recipient's DingTalk user ID. For a group message, `chatId` is the group's `openConversationId`. Thread targets and incoming robot Webhook URLs are not supported for proactive delivery. See [Webhook-triggered tasks](./overview#webhook-triggered-tasks) for the complete channel configuration and request format.

## Group Chats

DingTalk bots work in both DM and group conversations. To enable group support:

1. Set `groupPolicy` to `"allowlist"`, `"pairing"`, or `"open"` in your channel config
2. Add the bot to a DingTalk group
3. @mention the bot in the group to trigger a response
4. If using `groupPolicy: "pairing"`, approve the group's pairing request once before responses start

By default, the bot requires an @mention in group chats (`requireMention: true`). Set `"requireMention": false` for a specific group to make it respond to all messages. See [Group Chats](./overview#group-chats) for full details.

Set `"atSender": true` to have the bot @mention the member whose group message triggered its response. It is off by default and only applies to agent replies with a DingTalk staff ID. Replies are sent as DingTalk markdown whether or not they carry a mention; the mention prefix is included in the first message chunk.

Qwen Code preserves the text content supplied by DingTalk when constructing the canonical message; it does not remove a leading mention itself. When DingTalk omits the bot mention from a plain-text callback, a body such as `/clear` or `!command` still begins with that command marker and follows the normal local-command rules. When the callback retains a leading bot mention, as rich-text callbacks can, `@Bot /clear` and `@Bot !command` remain ordinary agent input because the canonical text does not begin with `/` or `!`. `isInAtList` continues to determine whether the group message addressed the bot.

### Finding a Group's Conversation ID

DingTalk uses `conversationId` to identify groups. You can find it in the channel service logs when someone sends a message in the group — look for the `conversationId` field in the log output.

## Images and Files

You can send photos and documents to the bot, not just text.

**Photos:** Send an image (screenshot, diagram, etc.) and the agent will analyze it using its vision capabilities. This requires a multimodal model — add `"model": "qwen3.5-plus"` (or another vision-capable model) to your channel config. DingTalk supports sending images directly or as part of rich text messages (mixed text + images).

**Files:** Send a PDF, code file, or any document. The bot downloads it from DingTalk's servers and saves it locally so the agent can read it with its file tools. Audio and video files are also supported. This works with any model.

**Generated files:** Ask the agent explicitly to send a completed local file and it can return the file as a native DingTalk attachment. Files must be non-empty, no larger than 20 MB, and located inside the configured workspace or the system temporary directory. One response can send at most five files. Upload or delivery failures are reported in the final text instead.

## Forwarded Chat Records

You can merge-forward a run of messages from another chat to the bot (DingTalk's "combined forward"), either as a message of its own or as the message you are replying to. The bot expands the record into text for the agent: the record's title and summary become a header line, and each forwarded message is listed under `[Chat record messages]` as `Sender: message`. A forwarded message whose body is not text is shown as a placeholder — `[image]`, `[file: <name>]`, `[audio]`, `[video]`.

Long records are **capped, and the cap is announced**: at most 50 messages, at most 4000 characters in total, and at most 500 characters per message. Whatever is cut is reported to the agent in the same text — a trailing `[N more message(s) not shown]` line for dropped messages, and a ` [truncated]` marker on any message that was shortened. So the agent knows it is answering about a partial record; if you need the whole thing, forward it in smaller batches.

A record you are **replying to** is quoted rather than sent, and quoted text is capped at 500 characters on every channel — so the record is rendered to that 500-character budget instead of the 4000-character one, and the same announcements apply within it. Expect a replied record to carry its header and the first message or two; forward it as its own message to give the agent the whole thing.

Because a forwarded record is written by people other than you, everything lifted out of it — titles, sender names, message bodies — is neutralized before it reaches the agent, so a forwarded message cannot pose as an instruction to the bot.

The multi-line layout above is what the agent sees in a 1:1 chat. In a group the whole message is neutralized a second time before it reaches the agent, which folds it onto one line and drops the square brackets around the markers; the content and the cap announcements are the same either way.

## Key Differences from Telegram

- **Authentication:** AppKey + AppSecret instead of a static bot token. The SDK manages access token refresh automatically.
- **Connection:** WebSocket stream instead of polling — no public IP or webhook URL needed.
- **Formatting:** Responses use DingTalk's markdown dialect. Markdown tables are passed through to the DingTalk client; long messages are split into chunks at ~3800 characters.
- **Working indicator:** A 👀 emoji reaction is added to the user's message while processing, then removed when the response is sent.
- **Media download:** Two-step process — a `downloadCode` from the message is exchanged for a temporary download URL via DingTalk's API.
- **Groups:** DingTalk uses `isInAtList` for @mention detection instead of parsing message entities.

## Tips

- **Use DingTalk markdown-aware instructions** — DingTalk supports headings, bold text, links, code blocks, and tables. Keep tables compact because narrow screens may scroll horizontally.
- **Restrict access** — In an organization context, `privatePolicy: "open"` may be acceptable. For tighter control, use `"allowlist"` or `"pairing"`. See [DM Pairing](./overview#dm-pairing) for details.
- **Referenced messages** — Quoting (replying to) a user message includes the quoted text as context for the agent. Rich-text quotes preserve their text order and attach embedded pictures. If the quoted message is a picture, file, audio, or video message, the bot downloads and attaches it the same way as when sent directly. Quoting bot responses is not yet supported.

## Troubleshooting

### Bot doesn't connect

- Verify your AppKey and AppSecret are correct
- Check that the environment variables are set before running `qwen channel start`
- Make sure **Stream Mode** is enabled in the bot's settings on the DingTalk Developer Portal
- Check the terminal output for connection errors

### Bot doesn't respond in groups

- Check that `groupPolicy` is set to `"allowlist"`, `"pairing"`, or `"open"` (default is `"disabled"`)
- If using `"pairing"`, verify the group's pairing request has been approved
- Make sure you @mention the bot in the group message
- Verify the bot has been added to the group

### "No sessionWebhook in message"

This means DingTalk didn't include a reply endpoint in the message callback. This can happen if the bot's permissions are misconfigured. Check the bot's settings in the Developer Portal.

### "Unable to process this message"

The reply identifies the failure category and suggests a next step. If the problem continues, give the bot administrator the reference shown in the reply; the same reference appears beside the detailed error in the channel process log.

---

### Users Features Channels Plugins (`docs/users/features/channels/plugins.md`)

# Custom Channel Plugins

You can extend the channel system with custom platform adapters packaged as [extensions](../../extension/introduction). This lets you connect Qwen Code to any messaging platform, webhook, or custom transport.

## How It Works

Channel plugins are loaded at startup from active extensions. When `qwen channel start` runs, it:

1. Scans all enabled extensions for `channels` entries in their `qwen-extension.json`
2. Dynamically imports each channel's entry point
3. Registers the channel type so it can be referenced in `settings.json`
4. Creates channel instances using the plugin's factory function

Your custom channel gets the full shared pipeline for free: sender gating, group policies, session routing, slash commands, crash recovery, and an agent bridge. Standalone `qwen channel start` currently supplies `AcpBridge`; plugin adapter code should depend on the adapter-facing `ChannelAgentBridge` contract. Existing TypeScript plugins with an explicit `AcpBridge` bridge parameter should migrate that annotation to `ChannelAgentBridge`; JavaScript plugins are unaffected at runtime.

## Installing a Custom Channel

Install an extension that provides a channel plugin:

```bash
# From a local path (for development or private plugins)
qwen extensions install /path/to/my-channel-extension

# Or link it for development (changes are reflected immediately)
qwen extensions link /path/to/my-channel-extension
```

## Configuring a Custom Channel

Add a channel entry to `~/.qwen/settings.json` using the custom type provided by the extension:

```json
{
  "channels": {
    "my-bot": {
      "type": "my-platform",
      "apiKey": "$MY_PLATFORM_API_KEY",
      "privatePolicy": "open",
      "cwd": "/path/to/project"
    }
  }
}
```

The `type` must match a channel type registered by an installed extension. Check the extension's documentation for which plugin-specific fields are required (e.g., `apiKey`, `webhookUrl`).

All standard channel options work with custom channels:

| Option          | Description                                                                                        |
| --------------- | -------------------------------------------------------------------------------------------------- |
| `privatePolicy` | `disabled`, `allowlist`, `pairing`, or `open`                                                      |
| `allowedUsers`  | Private-message allowlist of user IDs                                                              |
| `sessionScope`  | `user`, `chat_thread`, or `single`; legacy `thread` remains compatible for existing configurations |
| `cwd`           | Working directory for the agent                                                                    |
| `instructions`  | Prepended to the first message of each session                                                     |
| `model`         | Model override for the channel                                                                     |
| `groupPolicy`   | `disabled`, `allowlist`, `pairing`, or `open`                                                      |
| `dmPolicy`      | Deprecated private fallback: `open` or `disabled`                                                  |
| `groups`        | Per-group settings                                                                                 |

See [Overview](./overview) for details on each option.

## Starting the Channel

```bash
# Start all channels including custom ones
qwen channel start

# Start just your custom channel
qwen channel start my-bot
```

## What You Get for Free

Custom channels automatically support everything built-in channels do:

- **Sender policies** — `allowlist`, `pairing`, and `open` access control
- **Group policies** — Per-group settings with optional @mention gating
- **Session routing** — Per-user, per-thread, or single shared sessions
- **DM and group pairing** — Full pairing code flow for unknown users and groups
- **Slash commands** — `/help`, `/clear`, `/status` work out of the box
- **Custom instructions** — Prepended to the first message in each session
- **Crash recovery** — Automatic restart with session preservation
- **Per-session serialization** — Messages are queued to prevent race conditions

## Building Your Own Channel Plugin

Want to build a channel plugin for a new platform? See the [Channel Plugin Developer Guide](../../../developers/channel-plugins.md) for the `ChannelPlugin` interface, the `Envelope` format, and extension points.

---

### Users Features Channels Telegram (`docs/users/features/channels/telegram.md`)

# Telegram

This guide covers setting up a Qwen Code channel on Telegram.

## Prerequisites

- A Telegram account
- A Telegram bot token (see below)

## Creating a Bot

1. Open Telegram and search for [@BotFather](https://t.me/BotFather)
2. Send `/newbot` and follow the prompts to choose a name and username
3. BotFather will give you a bot token — save it securely

## Finding Your User ID

To use `privatePolicy: "allowlist"` or `"pairing"`, you need your Telegram user ID (a numeric ID, not your username).

The easiest way to find it:

1. Search for [@userinfobot](https://t.me/userinfobot) on Telegram
2. Send it any message — it will reply with your user ID

## Configuration

Add the channel to `~/.qwen/settings.json`:

```json
{
  "channels": {
    "my-telegram": {
      "type": "telegram",
      "token": "$TELEGRAM_BOT_TOKEN",
      "privatePolicy": "allowlist",
      "allowedUsers": ["YOUR_USER_ID"],
      "sessionScope": "user",
      "cwd": "/path/to/your/project",
      "instructions": "You are a concise coding assistant responding via Telegram. Keep responses short.",
      "groupPolicy": "disabled",
      "groups": {
        "*": { "requireMention": true }
      }
    }
  }
}
```

Set the bot token as an environment variable:

```bash
export TELEGRAM_BOT_TOKEN=<your-token-from-botfather>
```

Or add it to a `.env` file that gets sourced before running.

## Running

```bash
# Start only the Telegram channel
qwen channel start my-telegram

# Or start all configured channels together
qwen channel start
```

Then open your bot in Telegram and send a message. You should see "Working..." appear immediately, followed by the agent's response.

## Group Chats

To use the bot in Telegram groups:

1. Set `groupPolicy` to `"allowlist"`, `"pairing"`, or `"open"` in your channel config
2. **Disable privacy mode** in BotFather: `/mybots` → select your bot → Bot Settings → Group Privacy → Turn Off
3. Add the bot to a group. If it was already in the group, **remove and re-add it** (Telegram caches privacy settings from when the bot joined)
4. If using `groupPolicy: "allowlist"`, add the group's chat ID to `groups` in your config
5. If using `groupPolicy: "pairing"`, approve the group's pairing request once before responses start. Note that once a group is approved, **any member of that group** can use the bot by default (restrict with the group's `senders: "allowlist"` and `allowedUsers`); `privatePolicy` and the top-level `allowedUsers` do not gate members of an approved group.

By default, the bot requires an @mention or a reply to respond in groups. Set `"requireMention": false` for a specific group to make it respond to all messages (useful for dedicated task groups). See [Group Chats](./overview#group-chats) for full details.

## Images and Files

You can send photos and documents to the bot, not just text.

**Photos:** Send a photo and the agent will analyze it using its vision capabilities. This requires a multimodal model — add `"model": "qwen3.5-plus"` (or another vision-capable model) to your channel config. Photo captions are passed as the message text.

**Documents:** Send a PDF, code file, or any document. The bot downloads it and saves it locally so the agent can read it with its file tools. This works with any model. Telegram's file size limit is 20MB.

## Tips

- **Keep instructions concise-focused** — Telegram has a 4096-character message limit. Adding instructions like "keep responses short" helps the agent stay within bounds.
- **Use `sessionScope: "user"`** — This gives each user their own conversation. Use `/clear` to start fresh.
- **Restrict access** — Use `privatePolicy: "allowlist"` for a fixed set of users, or `"pairing"` to let new users request access with a code you approve via CLI. See [DM Pairing](./overview#dm-pairing) for details.

## Message Formatting

The agent's markdown responses are automatically converted to Telegram-compatible HTML. Code blocks, bold, italic, links, and lists are all supported.

## Troubleshooting

### Bot doesn't respond

- Check that the bot token is correct and the environment variable is set
- Verify your user ID is in `allowedUsers` if using `privatePolicy: "allowlist"`, or that you've been approved if using `"pairing"`
- Check the terminal output for errors

### Bot doesn't respond in groups

- Check that `groupPolicy` is set to `"allowlist"`, `"pairing"`, or `"open"` (default is `"disabled"`)
- If using `"allowlist"`, verify the group's chat ID is in the `groups` config
- If using `"pairing"`, verify the group's pairing request has been approved
- Make sure **Group Privacy is turned off** in BotFather — without this, the bot can't see non-command messages in groups
- If you changed privacy mode after adding the bot to a group, **remove and re-add the bot** to the group
- By default, the bot requires an @mention or a reply. Send `@yourbotname hello` to test

### "Sorry, something went wrong processing your message"

This usually means the agent encountered an error. Check the terminal output for details.

### Bot takes a long time to respond

The agent may be running multiple tool calls (reading files, searching, etc.). The "Working..." indicator shows while the agent is processing. Complex tasks can take a minute or more.

---

### Users Features Channels Weixin (`docs/users/features/channels/weixin.md`)

# WeChat (Weixin)

This guide covers setting up a Qwen Code channel on WeChat via the official iLink Bot API.

## Prerequisites

- A WeChat account that can scan QR codes (mobile app)
- Access to the iLink Bot platform (WeChat's official bot API)

## Setup

### 1. Log in via QR code

WeChat uses QR code authentication instead of a static bot token. Run the login command:

```bash
qwen channel configure-weixin
```

This will display a QR code URL. Scan it with your WeChat mobile app to authenticate. Your credentials are saved to `~/.qwen/channels/weixin/account.json`.

### 2. Configure the channel

Add the channel to `~/.qwen/settings.json`:

```json
{
  "channels": {
    "my-weixin": {
      "type": "weixin",
      "privatePolicy": "pairing",
      "allowedUsers": [],
      "sessionScope": "user",
      "cwd": "/path/to/your/project",
      "model": "qwen3.5-plus",
      "instructions": "You are a concise coding assistant responding via WeChat. Keep responses under 500 characters. Use plain text only."
    }
  }
}
```

Note: WeChat channels do not use a `token` field — credentials come from the QR login step.

### 3. Start the channel

```bash
# Start only the WeChat channel
qwen channel start my-weixin

# Or start all configured channels together
qwen channel start
```

Open WeChat and send a message to the bot. You should see a typing indicator ("...") while the agent processes, followed by the response.

## Images and Files

You can send photos and documents to the bot, not just text.

**Photos:** Send an image (screenshot, photo, etc.) and the agent will analyze it using its vision capabilities. This requires a multimodal model — add `"model": "qwen3.5-plus"` (or another vision-capable model) to your channel config. A typing indicator shows while the image is being downloaded and processed.

**Files:** Send a PDF, code file, or any document. The bot downloads and decrypts it from WeChat's CDN, saves it locally, and the agent reads it with its file tools. This works with any model.

## Configuration Options

WeChat channels support all the standard channel options (see [Channel Overview](./overview#options)), plus:

| Option    | Description                                                                    |
| --------- | ------------------------------------------------------------------------------ |
| `baseUrl` | Override the iLink Bot API base URL (default: `https://ilinkai.weixin.qq.com`) |

## Key Differences from Telegram

- **Authentication:** QR code login instead of a static bot token. Sessions can expire — the channel will pause and log a message if this happens.
- **Formatting:** WeChat only supports plain text. Markdown in agent responses is automatically stripped.
- **Typing indicator:** WeChat has a native "..." typing indicator instead of a "Working..." text message.
- **Groups:** WeChat iLink Bot is DM-only — group chats are not supported.
- **Media encryption:** Images and files are encrypted on WeChat's CDN with AES-128-ECB. The channel handles decryption transparently.

## Tips

- **Use plain text instructions** — Since WeChat strips all markdown, add instructions like "Use plain text only" to avoid the agent producing formatted responses that look messy.
- **Keep responses short** — WeChat message bubbles work best with concise text. Adding a character limit to your instructions helps (e.g., "Keep responses under 500 characters").
- **Session expiry** — If you see "Session expired (errcode -14)" in the logs, your WeChat login has expired. Stop the channel and re-run `qwen channel configure-weixin` to log in again.
- **Restrict access** — Use `privatePolicy: "pairing"` or `"allowlist"` to control who can talk to the bot. See [DM Pairing](./overview#dm-pairing) for details.

## Troubleshooting

### "WeChat account not configured"

Run `qwen channel configure-weixin` to log in via QR code first.

### "Session expired (errcode -14)"

Your WeChat login session has expired. Stop the channel and run `qwen channel configure-weixin` again.

### Bot doesn't respond

- Check the terminal output for errors
- Verify the channel is running (`qwen channel start my-weixin`)
- If using `privatePolicy: "allowlist"`, make sure your WeChat user ID is in `allowedUsers`

### Images not working

- Make sure your channel config has a `model` that supports vision (e.g., `qwen3.5-plus`)
- Check the terminal for CDN download errors — these may indicate a network issue

---

--- METRICS ---
- Files Extracted: 90
- Estimated Token Budget: ~469070 tokens
- Canonical Reference: https://codewiki.google/github.com/QwenLM/qwen-code
