Skip to content

Support disabling slash commands via settings (slashCommands.disabled) #3444

@ihubanov

Description

@ihubanov

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.

Settingpackages/cli/src/config/settingsSchema.ts:

{
  "slashCommands": {
    "disabled": ["auth", "mcp", "extensions", "ide", "quit"]
  }
}
  • 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 varQWEN_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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions