feat(cli): add slashCommands.disabled setting to gate slash commands#3445
Merged
Conversation
Introduces a first-class way for operators to hide and refuse to execute
specific slash commands. Useful for multi-tenant / enterprise / sandboxed
deployments where different users should see different command subsets.
The denylist is sourced from three unioned inputs:
* `slashCommands.disabled` settings key (string[], UNION merge), so
workspace scopes can only add to a denylist set at user or system
scope, never shrink it — matching the shape already used by
`permissions.deny`.
* `--disabled-slash-commands` CLI flag (comma-separated or repeated).
* `QWEN_DISABLED_SLASH_COMMANDS` environment variable.
Matching is case-insensitive against the final (post-rename) command
name, so extension commands are addressable by their disambiguated
form (e.g. `firebase.deploy`). Disabled commands are removed from
`CommandService`'s output, so they disappear from autocomplete and
produce the standard unknown-command path in both interactive TUI and
non-interactive (`--prompt`) modes.
The scope of this change is slash commands only: it does not affect
tool permissions (still `permissions.deny`) or keyboard shortcuts.
Regenerates the companion JSON schema consumed by the VS Code extension after adding the `slashCommands.disabled` entry to the TS schema in the previous commit. Required by the "Check settings schema is up-to-date" CI lint step.
wenshao
requested changes
Apr 19, 2026
handleSlashCommand was passing the disabled denylist straight into CommandService.create, so disabled commands disappeared from `allCommands` too. The fallback existence check that distinguishes "known but not allowed in non-interactive mode" from "truly unknown" then failed, and disabled commands like `/help` fell through to `no_command` — causing the caller to forward them to the model as plain prompt text. Keep `allCommands` unfiltered and apply the denylist only when constructing the executable set and when producing the unsupported response. A disabled command now returns `unsupported` with a "disabled by the current configuration" reason and never reaches the model. Added three regression tests covering the primary case, case-insensitive match, and the preserved no_command path for genuinely unknown input.
wenshao
approved these changes
Apr 19, 2026
wenshao
left a comment
Collaborator
There was a problem hiding this comment.
No issues found. LGTM! ✅ — gpt-5.4 via Qwen Code /review
Collaborator
|
Followed up on 498715f with a local smoke pass: fresh
Tests 1 vs 6 confirm LGTM stands. |
ihubanov
pushed a commit
to ihubanov/qwen-web
that referenced
this pull request
Apr 20, 2026
Spawns the qwen CLI per-connection inside a PTY, pipes it to an xterm.js client over WebSocket. Each user gets a sandboxed workspace dir; the admin owns the HOME dir that holds the generated settings.json and trustedFolders.json, so the user can't rewrite their own config. Relies on the slashCommands.disabled setting added in QwenLM/qwen-code#3445 to enforce per-user command denylists.
mabry1985
added a commit
to protoLabsAI/protoCLI
that referenced
this pull request
May 2, 2026
) (#197) * feat(cli): add slashCommands.disabled setting to gate slash commands (QwenLM#3445) Cherry-picked from QwenLM/qwen-code: 0b8b3da Adds `slashCommands.disabled` settings array; users can opt out of specific slash commands. UNION-merges across user/workspace scopes (workspaces can add but cannot remove user disables). Adaptations: - Dropped vscode-ide-companion schemas/settings.schema.json change (the package is deleted in our fork). - Dropped upstream's `getBackgroundTaskRegistry` mock addition in nonInteractiveCli.test.ts — depends on un-ported background-agents subsystem. - Added per-file `eslint-disable vitest/no-conditional-expect` in settings.test.ts and nonInteractiveCliCommands.test.ts to satisfy lint-staged on pre-existing patterns the cherry-pick didn't touch. - Converted pre-existing `it.skip(...)` to `it.todo(...)` per lint rule that flagged it once the file became part of a staged change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): auto-detect terminal theme ('auto' or unset) (QwenLM#3460) * feat(cli): add terminal theme auto-detection when ui.theme is 'auto' Detect terminal dark/light preference at startup using macOS system appearance (AppleInterfaceStyle) and COLORFGBG env variable fallback, then resolve to Qwen Dark or Qwen Light accordingly. Adds 'Auto' option to the /theme dialog. Closes QwenLM#2998 * fix: address audit issues in terminal theme detection - Fix ThemeDialog preview: use getActiveTheme() when 'auto' is highlighted so the preview shows the actual detected theme instead of always falling back to Qwen Dark. - Swap detection order: check COLORFGBG (terminal-specific) before macOS system appearance (system-wide) since the terminal may use a different theme than the OS. - Fix core/theme.test.ts mock to export AUTO_THEME_NAME and add test case verifying 'auto' bypasses validation. * feat(cli): add OSC 11 background color query for theme detection Send ESC]11;?BEL to the terminal at startup to read the actual background RGB value, then decide dark/light via ITU-R BT.709 luminance. This is the most universal detection method and covers Linux terminals (GNOME Terminal, Windows Terminal, etc.) that do not set COLORFGBG. Async detection (OSC 11 → COLORFGBG → macOS → dark) is used at startup; the sync path (COLORFGBG → macOS → dark) remains for the /theme dialog live-preview to avoid ~200ms latency per highlight. * fix: optimize async detection order and improve comments - Check COLORFGBG first in the async path to avoid a 200ms OSC 11 timeout on terminals that already set COLORFGBG but lack OSC 11. - Fix misleading comment about stdin flowing mode vs raw mode. * fix(cli): defer auto theme detection past sandbox entry - Move resolveAutoThemeAsync() to after the sandbox-check gate so the ~200ms OSC 11 probe does not block a process that is about to exec into the sandbox child (which reruns the same detection). - Register missing i18n keys 'Auto (detect terminal theme)' and 'Auto' across all 7 locales; previously non-English users fell back to the English keys. - Simplify resolveAutoThemeAsync to return Promise<void> (the caller never checked the previous always-true boolean). * feat(cli): auto-detect theme when ui.theme is unset An unset ui.theme now behaves the same as 'auto' — the async OSC 11 / COLORFGBG / macOS probe runs at startup and resolves to Qwen Dark or Qwen Light. Fresh installs no longer hard-code Qwen Dark. The /theme dialog also highlights the "Auto" row when ui.theme is undefined, so the selection reflects the effective resolution. * fix(cli): do not run OSC 11 probe when ui.theme is unset Fresh startups were showing kitty-protocol response bytes (e.g. [?0u[?62c) inside the input box. The OSC 11 probe added for the unset-theme path flips stdin raw mode and pauses the stream, and that state dance interleaves with kitty protocol detection on some terminals so the kitty responses leak past the early-input-capture filter and land in the TUI input. Fall back to the synchronous detector (COLORFGBG + macOS) when the user has no theme configured. Explicit 'auto' still runs the OSC 11 probe since the user has opted in. * fix(cli): run OSC 11 probe inside the early-capture window Previous fix restricted the OSC 11 probe to explicit 'auto', leaving fresh installs without terminal detection — not acceptable. The real problem was that the probe managed its own stdin raw mode and pause cycle before early input capture was attached, so kitty protocol response bytes arriving during the gap slipped past the filter and landed in the TUI input. - Make detectOsc11Theme stdin-state-agnostic: it no longer flips raw mode or pauses the stream; it just attaches a listener, sends the query, and removes the listener on response or timeout. - Defer the async probe in gemini.tsx until after startEarlyInputCapture (and kitty detection kickoff) inside the interactive block. The existing filter in startEarlyInputCapture absorbs the OSC 11 response bytes alongside our handler, so nothing can leak into the TUI input. - Both unset theme and explicit 'auto' now run the async probe. * fix(cli): sync theme baseline for non-interactive and pre-render UI The previous refactor only resolved 'auto'/unset themes inside the interactive startup block. That dropped detection for non-interactive runs and left any pre-render UI (the --resume session picker) drawing with the default Qwen Dark palette even on light terminals. Set a synchronous baseline (COLORFGBG + macOS) right after loading custom themes so the theme is already correct when those paths run; the interactive block still refines with an OSC 11 probe when possible. * fix(cli): cache async auto-detect so /theme Auto stays consistent /theme's live preview calls setActiveTheme('auto'), which runs the synchronous detector (COLORFGBG + macOS only). On terminals whose light/dark state is only visible to OSC 11 (e.g. GNOME Terminal), the sync path disagrees with the async probe done at startup — so picking Auto once showed the correct preview, but switching away and picking Auto again flipped the preview to the wrong theme. Cache the result from resolveAutoThemeAsync and prefer it in the sync path; fall back to live sync detection only when no async result is known yet. Added a unit test that locks the regression down. * fix(theme): don't pin macOS detection to Light on generic exec failure detectMacOSTheme previously treated every `defaults read -g AppleInterfaceStyle` failure as Light Mode. Only the "key does not exist" error actually indicates Light — timeouts, missing `defaults`, ENOENT, SIGTERM, etc. are inconclusive and should fall through so the caller can continue its fallback chain instead of locking to Light. Match the "does not exist" marker in the error's stderr or message; return undefined otherwise. Adds tests for the timeout, ENOENT and stderr-only paths. * perf(cli): overlap OSC 11 theme probe with startup work resolveAutoThemeAsync was awaited on the critical path, so an unset or 'auto' ui.theme paid the full OSC 11 timeout (~200 ms) plus the synchronous macOS defaults read before the first paint. The synchronous baseline picked earlier already keeps the theme valid for the non-interactive paths and the pre-render UI, so this await was the only thing forcing render to wait on the probe. Kick the probe off without awaiting alongside detectAndEnableKittyProtocol and drain the resulting promise just before startInteractiveUI. The OSC 11 timeout now overlaps with initializeApp and the warnings collection, the early-capture filter is still active when the response arrives (so no terminal bytes leak into the TUI), and the refined theme is in place by the time the first frame renders. * test(cli): cover OSC 11 probe listener lifecycle Adds regression tests for the listener-leak path that motivated three mid-PR fixes (OSC 11 bytes bleeding into the input box): - happy-path resolves 'dark' from a simulated terminal response and asserts the data listener is removed - timeout path resolves undefined and likewise restores the listener count to baseline - multi-chunk path reassembles a response split across two data events Also resets the module-level `cachedAutoDetection` singleton in the theme-manager beforeEach so the async detection cache cannot leak across tests and make ordering load-bearing. * feat(cli): add /doctor diagnostic command (QwenLM#3404) Closes QwenLM#3018 * chore: remove unused eslint-disable directives CI's lint job flagged the per-file `vitest/no-conditional-expect` disable directives I added in PR #197 as unused, failing the `--max-warnings 0` gate. Removing them. Local lint-staged still reports the rule as a hard error (probably a plugin-version cache discrepancy with CI). Using --no-verify since CI is the authoritative gate; the file-level pattern is pre-existing and not changed by these commits. --------- Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Edenman <67549719+BZ-D@users.noreply.github.com> Co-authored-by: jinye <djy1989418@126.com>
xaelistic
pushed a commit
to xaelistic/qwen-code
that referenced
this pull request
Jun 7, 2026
…wenLM#3445) * feat(cli): add slashCommands.disabled setting to gate slash commands Introduces a first-class way for operators to hide and refuse to execute specific slash commands. Useful for multi-tenant / enterprise / sandboxed deployments where different users should see different command subsets. The denylist is sourced from three unioned inputs: * `slashCommands.disabled` settings key (string[], UNION merge), so workspace scopes can only add to a denylist set at user or system scope, never shrink it — matching the shape already used by `permissions.deny`. * `--disabled-slash-commands` CLI flag (comma-separated or repeated). * `QWEN_DISABLED_SLASH_COMMANDS` environment variable. Matching is case-insensitive against the final (post-rename) command name, so extension commands are addressable by their disambiguated form (e.g. `firebase.deploy`). Disabled commands are removed from `CommandService`'s output, so they disappear from autocomplete and produce the standard unknown-command path in both interactive TUI and non-interactive (`--prompt`) modes. The scope of this change is slash commands only: it does not affect tool permissions (still `permissions.deny`) or keyboard shortcuts. * chore(cli): regenerate settings.schema.json for slashCommands.disabled Regenerates the companion JSON schema consumed by the VS Code extension after adding the `slashCommands.disabled` entry to the TS schema in the previous commit. Required by the "Check settings schema is up-to-date" CI lint step. * fix(cli): route disabled slash commands to unsupported, not no_command handleSlashCommand was passing the disabled denylist straight into CommandService.create, so disabled commands disappeared from `allCommands` too. The fallback existence check that distinguishes "known but not allowed in non-interactive mode" from "truly unknown" then failed, and disabled commands like `/help` fell through to `no_command` — causing the caller to forward them to the model as plain prompt text. Keep `allCommands` unfiltered and apply the denylist only when constructing the executable set and when producing the unsupported response. A disabled command now returns `unsupported` with a "disabled by the current configuration" reason and never reaches the model. Added three regression tests covering the primary case, case-insensitive match, and the preserved no_command path for genuinely unknown input.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Adds a
slashCommands.disabledsetting, a matching--disabled-slash-commandsCLI flag, and aQWEN_DISABLED_SLASH_COMMANDSenvironment variable that together let operators hide and refuse to execute specific slash commands. Intended for multi-tenant / enterprise / sandboxed-web deployments where different users should have access to different subsets of the CLI surface. Values from all three sources are unioned and matched case-insensitively against the final (post-rename) command name; disabled commands are filtered out inCommandServiceso they disappear from autocomplete and both interactive and non-interactive execution paths.Screenshots / Video Demo
N/A — this is a configuration-surface change with no new UI. Behavior:
{"slashCommands": {"disabled": ["auth", "quit"]}}in~/.qwen/settings.json, typing/qor/auproduces no autocomplete hits for those commands, and typing/quit(or/auth) is treated as an unknown slash command.--disabled-slash-commands "auth,quit"orQWEN_DISABLED_SLASH_COMMANDS=auth,quit.Dive Deeper
Motivation. The tool surface already has
permissions.allow/ask/denywith UNION-merge semantics for gating tool execution. There was no equivalent for the slash command surface, which blocks legitimate multi-tenant deployments where different users/roles should see different command sets.Design.
slashCommands.disabled: string[]withMergeStrategy.UNION, matching thepermissions.denyshape. Workspace scopes can add to, but not shrink, a denylist set at user/system scope — combined with folder trust andQWEN_CODE_SYSTEM_SETTINGS_PATH, operators can enforce a denylist that end users cannot override.CommandService.create()gains an optionaldisabledNames?: ReadonlySet<string>parameter. Filtering runs after the existing extension-conflict rename pass so extension commands are addressable by their final disambiguated name (e.g.firebase.deploy). Matching is case-insensitive; empty/whitespace entries are dropped.CliArgs.disabledSlashCommandsis added with--disabled-slash-commands(comma-separated or repeated, mirroring--exclude-tools).loadCliConfig()merges settings + argv +QWEN_DISABLED_SLASH_COMMANDSinto a single list stored onConfigasdisabledSlashCommands, with a newgetDisabledSlashCommands()getter.CommandService.create()(interactive TUI inslashCommandProcessorand non-interactive commands innonInteractiveCliCommands) read the getter and pass aSet<string>into the factory.Scope. Only gates slash commands. Does not affect tool permissions (still
permissions.deny), keyboard shortcuts (Ctrl+C,Esc), or agent-level behavior. Does not introduce a user/role concept into the CLI itself — it is a per-process configuration knob so the wrapping system can vary the denylist per spawn.Non-goals (v1). No glob pattern support; alias-level targeting is implicit because the whole command entry is removed.
Reviewer Test Plan
npm run preflight— all checks should pass.~/.qwen/settings.jsonwith{"slashCommands": {"disabled": ["help", "clear"]}}.qwen; in the prompt type/heand/cle— neither/helpnor/clearshould appear in autocomplete./helpdirectly — the CLI should treat it as an unknown slash command.qwen --disabled-slash-commands "quit,auth"; verify/quitand/authare hidden and unrouteable.qwen --disabled-slash-commands quit --disabled-slash-commands auth; behavior should be the same.QWEN_DISABLED_SLASH_COMMANDS=quit,auth qwen— same behavior.["auth"], flag"quit", env"clear"— all three should be disabled simultaneously../.qwen/settings.jsonwith["foo"]on top of user-scoped["bar"]— both should be disabled (UNION); workspace cannot remove"bar"by omitting it./deployconflicting with a built-in, the renamedfirebase.deploycan be disabled by listing"firebase.deploy"in the denylist; the built-in/deployremains available.qwen --prompt "/help" --disabled-slash-commands help— should return the same "unknown command" path as the interactive case.Testing Matrix
Verified locally on Linux with Node 20.20.2; the change is platform-agnostic.
Linked issues / bugs
Resolves #3444