Summary
Add a first-class mechanism for administrators to disable specific slash commands per deployment, via settings, CLI flag, and environment variable. This is needed to lock down the CLI surface in multi-tenant / enterprise / sandboxed-web deployments where different users should have access to different subsets of slash commands.
Motivation
Qwen Code's tool surface already has permissions.allow / permissions.ask / permissions.deny with UNION-merge semantics, which works well for gating tool execution. There is no equivalent for the slash command surface. Concretely, today there is no supported way to:
- Hide
/auth, /mcp, /extensions, /ide, /quit, etc. from a user whose role shouldn't see them.
- Enforce a command denylist from a system-level
settings.json that end users can't override or shrink.
- Disable commands per session via a CLI flag when spawning the CLI as a subprocess (e.g. from a web wrapper).
Today the only knobs are:
- Renaming / removing files under
~/.qwen/commands/ or ./.qwen/commands/ (only affects file-defined commands, not the ~35 built-ins).
- Disabling an entire extension (coarse-grained, per-extension, not per-command).
Client-side filtering in a wrapper (e.g. dropping keystrokes that look like /quit before they reach the PTY) is unreliable — paste, cursor edits, and arbitrary text composition bypass it. The restriction must be enforced inside the CLI process.
Proposal
Introduce a new setting plus a matching CLI flag and env var, all unioned into a single denylist consumed by CommandService at load time.
Setting — packages/cli/src/config/settingsSchema.ts:
- Type:
string[]
mergeStrategy: UNION — matches the existing pattern on permissions.deny. Workspace settings can add to the denylist but cannot shrink a list set by user/system scope. Combined with the existing folder-trust + system-settings mechanism, this lets operators enforce a denylist that end users can't override.
CLI flag — --disabled-slash-commands:
qwen --disabled-slash-commands "auth,mcp,extensions"
Accepts comma-separated values or repeated occurrences, mirroring the existing --exclude-tools / --allowed-tools shape.
Env var — QWEN_DISABLED_SLASH_COMMANDS:
QWEN_DISABLED_SLASH_COMMANDS=auth,mcp qwen
Useful for web wrappers / sidecar launchers that spawn the CLI per user role.
Semantics
- Matching is case-insensitive against the final command name (i.e. the post-disambiguation name, so extension commands are addressed by
extensionName.commandName, e.g. firebase.deploy).
- Whitespace is trimmed; empty entries are ignored.
- The three sources (settings, CLI flag, env var) are unioned and de-duplicated.
- Disabled commands are removed from
CommandService's output entirely, so they:
- Do not appear in
/-autocomplete.
- Are not routed to by
parseSlashCommand() (typing them yields "unknown command").
- Apply to both interactive TUI and non-interactive (
--prompt) modes.
- Non-goals (v1): glob patterns (can be added later if requested), alias-level targeting (disabling a command disables all its aliases, since the whole command entry is removed — which matches the user expectation).
Scope
- Only gates slash commands. It does not affect tool permissions (
permissions.deny remains the mechanism for that) or keyboard shortcuts (Ctrl+C, Esc, etc.).
- Does not introduce any user/role concept into the CLI itself — it's a per-process configuration knob, leaving multi-user identity concerns to the wrapping system.
Alternatives considered
- Extension-only enable/disable — already exists, but can only gate whole extensions, not built-in commands or individual file commands.
- Wrapper-side keystroke filtering — unreliable and trivially bypassable.
- A
permissions.slashCommands.deny nesting — would conflate UI-level commands with tool-execution permissions, which are conceptually distinct (commands don't produce tool-invocation side effects by themselves).
A separate top-level slashCommands block was chosen for clarity and because it gives a natural home for future slash-command-level knobs (e.g. autocomplete config).
Willing to implement
Yes — I have a branch ready with:
- Schema entry (
slashCommands.disabled, UNION merge).
CommandService.create() accepting an optional disabledNames?: ReadonlySet<string> parameter, filtering case-insensitively after the existing extension-conflict rename pass.
- CLI flag + env var + settings merged in
loadCliConfig() and plumbed through Config.getDisabledSlashCommands().
- Both interactive and non-interactive call sites updated.
- Unit tests in
CommandService.test.ts (filtering, case-insensitivity, whitespace, extension-renamed names, no-op when undefined/empty) and settings.test.ts (UNION merge across scopes).
- Documentation updates in
docs/users/configuration/settings.md.
Happy to open a draft PR for review once this direction is confirmed, or to adjust the shape based on feedback first.
Summary
Add a first-class mechanism for administrators to disable specific slash commands per deployment, via settings, CLI flag, and environment variable. This is needed to lock down the CLI surface in multi-tenant / enterprise / sandboxed-web deployments where different users should have access to different subsets of slash commands.
Motivation
Qwen Code's tool surface already has
permissions.allow/permissions.ask/permissions.denywith UNION-merge semantics, which works well for gating tool execution. There is no equivalent for the slash command surface. Concretely, today there is no supported way to:/auth,/mcp,/extensions,/ide,/quit, etc. from a user whose role shouldn't see them.settings.jsonthat end users can't override or shrink.Today the only knobs are:
~/.qwen/commands/or./.qwen/commands/(only affects file-defined commands, not the ~35 built-ins).Client-side filtering in a wrapper (e.g. dropping keystrokes that look like
/quitbefore they reach the PTY) is unreliable — paste, cursor edits, and arbitrary text composition bypass it. The restriction must be enforced inside the CLI process.Proposal
Introduce a new setting plus a matching CLI flag and env var, all unioned into a single denylist consumed by
CommandServiceat load time.Setting —
packages/cli/src/config/settingsSchema.ts:{ "slashCommands": { "disabled": ["auth", "mcp", "extensions", "ide", "quit"] } }string[]mergeStrategy: UNION— matches the existing pattern onpermissions.deny. Workspace settings can add to the denylist but cannot shrink a list set by user/system scope. Combined with the existing folder-trust + system-settings mechanism, this lets operators enforce a denylist that end users can't override.CLI flag —
--disabled-slash-commands:Accepts comma-separated values or repeated occurrences, mirroring the existing
--exclude-tools/--allowed-toolsshape.Env var —
QWEN_DISABLED_SLASH_COMMANDS:Useful for web wrappers / sidecar launchers that spawn the CLI per user role.
Semantics
extensionName.commandName, e.g.firebase.deploy).CommandService's output entirely, so they:/-autocomplete.parseSlashCommand()(typing them yields "unknown command").--prompt) modes.Scope
permissions.denyremains the mechanism for that) or keyboard shortcuts (Ctrl+C,Esc, etc.).Alternatives considered
permissions.slashCommands.denynesting — would conflate UI-level commands with tool-execution permissions, which are conceptually distinct (commands don't produce tool-invocation side effects by themselves).A separate top-level
slashCommandsblock was chosen for clarity and because it gives a natural home for future slash-command-level knobs (e.g. autocomplete config).Willing to implement
Yes — I have a branch ready with:
slashCommands.disabled, UNION merge).CommandService.create()accepting an optionaldisabledNames?: ReadonlySet<string>parameter, filtering case-insensitively after the existing extension-conflict rename pass.loadCliConfig()and plumbed throughConfig.getDisabledSlashCommands().CommandService.test.ts(filtering, case-insensitivity, whitespace, extension-renamed names, no-op when undefined/empty) andsettings.test.ts(UNION merge across scopes).docs/users/configuration/settings.md.Happy to open a draft PR for review once this direction is confirmed, or to adjust the shape based on feedback first.