# Qwen Code Permissions & Control Model, Explained

Qwen Code (Alibaba, Gemini CLI lineage) ships five approval modes, Claude-style permission rules, an optional sandbox, and a hook system that is Claude Code's contract. What each layer actually gates, what the default auto mode decides for you, and where headless runs resolve an ask.

# The Qwen Code control model, explained

[Qwen Code](https://github.com/QwenLM/qwen-code) is Alibaba's open-source terminal coding agent, a Gemini CLI fork tuned for Qwen models and usable with any OpenAI-compatible endpoint. It is larger than its profile suggests: about 27,600 GitHub stars and roughly 85,000 npm downloads a week as of early September 2026, and the heaviest wrapper we have measured: [99 KB before your prompt](/blog/what-coding-harnesses-send-before-your-prompt), 27 tools whose names mirror Claude Code's in snake_case. Its control surface mirrors Claude Code too, which is the useful part.

*This page covers Qwen Code's own model. For adding ACP to Qwen Code, see the [install guide](/integrations/qwen-code); for the cross-harness picture, [the controls comparison](/controls).*

## Layer 1: approval modes

`tools.approvalMode` in `settings.json`, or `--approval-mode` / `--yolo` on the command line:

| Mode | What runs without asking |
|---|---|
| `plan` | Nothing that modifies files or runs commands |
| `default` | Nothing. Every edit and shell command prompts |
| `auto-edit` | File edits. Commands still prompt |
| `auto` **(default)** | Whatever an LLM classifier judges safe. Risky actions are blocked |
| `yolo` | Everything |

The default deserves a second look. Out of the box, the thing deciding whether a shell command is safe is a model call, not a rule you wrote. That is the same design as fx's reviewer and Gemini CLI's, and it has the same property: it is a judgment, not a policy, and it is not reproducible. Teams that want a deterministic boundary set `default` or `auto-edit` and express the rest as rules.

## Layer 2: permission rules

The `permissions` block is Claude Code's rule grammar:

```json
{
  "permissions": {
    "allow": ["Bash(git status)", "Read(./src/**)"],
    "ask":   ["Bash(git push *)"],
    "deny":  ["Read(./secrets/**)", "Bash(rm -rf *)", "mcp__untrusted"]
  }
}
```

Priority is `deny` over `ask` over `allow`. Rules are managed with `/permissions` in the session. The older `tools.allowed` and `tools.exclude` keys are deprecated in favor of this block but still honoured at startup, so an audit of an existing install should read both. `tools.core` restricts the built-in registry to an allowlist and fails closed for built-ins, but MCP, skill and synthetic tools bypass it.

Settings resolve in a fixed order: defaults, then the system defaults file, user `~/.qwen/settings.json`, project `.qwen/settings.json`, then the system settings file at `/etc/qwen-code/settings.json`, then environment, then flags. System settings override user and project, which is the enterprise pin: an operator can fix the approval mode and deny list for every user on a machine.

## Layer 3: sandbox

`tools.sandbox` accepts a boolean or a path. Docker and Podman are the container backends, with `tools.sandboxImage` or `--sandbox-image` selecting the image; on macOS a seatbelt profile can be supplied per project as `.qwen/sandbox-macos-custom.sb`. Off by default. As with every harness, the sandbox bounds *where* the agent can act; it does not decide *which* actions run.

## Layer 4: hooks, and why they matter here

Qwen Code's hook system is Claude Code's contract, almost field for field:

| | Qwen Code | Claude Code |
|---|---|---|
| Events | `PreToolUse`, `PostToolUse`, `SessionStart`, `Stop`, `UserPromptSubmit`, `PermissionRequest`, `PermissionDenied`, plus subagent, compaction and todo events | Same core set |
| stdin | `session_id`, `transcript_path`, `cwd`, `hook_event_name`, `tool_name`, `tool_input`, `tool_use_id`, `permission_mode`; `tool_response` on PostToolUse | Same |
| Decision | `hookSpecificOutput.permissionDecision`: `allow` / `ask` / `deny`, with `permissionDecisionReason`; exit code 2 blocks with stderr as the reason | Same |
| Matcher | Regex on the runtime tool id; `*` or empty matches all | Regex; `.*` |
| Timeout | **milliseconds**, default 60,000 | seconds |
| Location | `hooks` in `~/.qwen/settings.json` or `.qwen/settings.json` | `~/.claude/settings.json` |
| Off switch | `disableAllHooks: true` | none |

Two consequences. First, a hook written for Claude Code runs on Qwen Code with a client-name change and a timeout in the right unit, which is what the [ACP install](/integrations/qwen-code) does. Second, hooks are the one layer that sees every tool with its arguments before it runs, across all five approval modes, including `yolo`. The permission rules gate the built-in prompts; the hook gates the call.

Project-level hooks require the folder to be trusted (`security.folderTrust`). User-level hooks in `~/.qwen/settings.json` do not, which is why the installer writes there.

## The empty chair

Qwen Code states the unattended case in its own docs: a hook's `ask` **degrades to `deny`** in headless runs and background subagents. That is the correct posture and it is rare to see it written down. Combined with `tools.approvalMode`, a `qwen --prompt` run in CI has a known answer for every call: rules and hooks decide, and anything that would have needed a human is refused rather than waved through.

The gap is the same as everywhere else. Qwen Code records nothing durable about what was allowed, refused or attempted; the decision happens and is gone. A hook that reports each decision to an independent record closes it.

## Where Qwen Code sits

Against the [comparison table](/controls): a full native permission model (five modes plus rules), an optional sandbox, a Claude-contract hook surface with deny, ask and output rewrite, a documented headless posture, and an enterprise pin via system settings. Its escape hatch is `--yolo`, which the hook still sees. The thing to know before trusting the default is that `auto` mode's safety judgment is a model's.
