fix(env): add key sanitization and shell completions

- Validate env var keys match ^[A-Za-z_][A-Za-z0-9_]*$ before output
  to prevent injection via crafted config files
- Add env command to all 4 shell completion scripts (bash, zsh, fish,
  PowerShell) with sub-completions for --format and --shell flags
This commit is contained in:
Tam Nhu Tran
2026-02-11 06:36:51 +07:00
parent a5dc15d174
commit 76457a567d
5 changed files with 117 additions and 21 deletions
+28 -1
View File
@@ -18,7 +18,7 @@ _ccs_completion() {
# Top-level completion (first argument)
if [[ ${COMP_CWORD} -eq 1 ]]; then
local commands="auth api cliproxy doctor sync update"
local commands="auth api cliproxy doctor env sync update"
local flags="--help --version --shell-completion -h -v -sc"
local cliproxy_profiles="gemini codex agy qwen"
local profiles=""
@@ -151,6 +151,33 @@ _ccs_completion() {
esac
fi
# env subcommands
if [[ ${COMP_WORDS[1]} == "env" ]]; then
case "${prev}" in
env)
# Complete with profile names and flags
local env_opts="--format --shell --help -h $cliproxy_profiles"
if [[ -f ~/.ccs/config.json ]]; then
env_opts="$env_opts $(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null || true)"
fi
COMPREPLY=( $(compgen -W "${env_opts}" -- ${cur}) )
return 0
;;
--format)
COMPREPLY=( $(compgen -W "openai anthropic raw" -- ${cur}) )
return 0
;;
--shell)
COMPREPLY=( $(compgen -W "bash fish powershell" -- ${cur}) )
return 0
;;
*)
COMPREPLY=( $(compgen -W "--format --shell --help -h" -- ${cur}) )
return 0
;;
esac
fi
# Flags for doctor command
if [[ ${COMP_WORDS[1]} == "doctor" ]]; then
COMPREPLY=( $(compgen -W "--help -h" -- ${cur}) )
+25 -17
View File
@@ -121,33 +121,34 @@ complete -c ccs -s v -l version -d 'Show version information'
complete -c ccs -s sc -l shell-completion -d 'Install shell completion'
# Commands - grouped with [cmd] prefix for visual distinction
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'auth' -d '[cmd] Manage multiple Claude accounts'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'api' -d '[cmd] Manage API profiles (create/remove)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'cliproxy' -d '[cmd] Manage CLIProxy variants and binary'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'doctor' -d '[cmd] Run health check and diagnostics'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'sync' -d '[cmd] Sync delegation commands and skills'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'update' -d '[cmd] Update CCS to latest version'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'auth' -d '[cmd] Manage multiple Claude accounts'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'api' -d '[cmd] Manage API profiles (create/remove)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'cliproxy' -d '[cmd] Manage CLIProxy variants and binary'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'doctor' -d '[cmd] Run health check and diagnostics'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'env' -d '[cmd] Export env vars for third-party tools'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'sync' -d '[cmd] Sync delegation commands and skills'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'update' -d '[cmd] Update CCS to latest version'
# CLIProxy profiles - grouped with [proxy] prefix for OAuth providers
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'gemini' -d '[proxy] Google Gemini (OAuth)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'codex' -d '[proxy] OpenAI Codex (OAuth)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'agy' -d '[proxy] Antigravity (OAuth)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'qwen' -d '[proxy] Qwen Code (OAuth)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'gemini' -d '[proxy] Google Gemini (OAuth)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'codex' -d '[proxy] OpenAI Codex (OAuth)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'agy' -d '[proxy] Antigravity (OAuth)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'qwen' -d '[proxy] Qwen Code (OAuth)'
# Model profiles - grouped with [model] prefix for visual distinction
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'default' -d '[model] Default Claude Sonnet 4.5'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'glm' -d '[model] GLM-4.6 (cost-optimized)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'glmt' -d '[model] GLM-4.6 with thinking mode'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'kimi' -d '[model] Kimi for Coding (long-context)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'default' -d '[model] Default Claude Sonnet 4.5'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'glm' -d '[model] GLM-4.6 (cost-optimized)'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'glmt' -d '[model] GLM-4.6 with thinking mode'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'kimi' -d '[model] Kimi for Coding (long-context)'
# Custom model profiles - dynamic with [model] prefix
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a '(__fish_ccs_get_custom_settings_profiles)' -d '[model] Settings-based profile'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a '(__fish_ccs_get_custom_settings_profiles)' -d '[model] Settings-based profile'
# CLIProxy variants - dynamic with [variant] prefix
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a '(__fish_ccs_get_cliproxy_variants)' -d '[variant] CLIProxy variant'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a '(__fish_ccs_get_cliproxy_variants)' -d '[variant] CLIProxy variant'
# Account profiles - dynamic with [account] prefix
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a '(__fish_ccs_get_account_profiles)' -d '[account] Account-based profile'
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a '(__fish_ccs_get_account_profiles)' -d '[account] Account-based profile'
# shell-completion subflags
complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l bash -d 'Install for bash'
@@ -171,6 +172,13 @@ complete -c ccs -n '__fish_seen_subcommand_from update' -s h -l help -d 'Show he
# doctor command flags
complete -c ccs -n '__fish_seen_subcommand_from doctor' -s h -l help -d 'Show help for doctor command'
# env command completions
complete -c ccs -n '__fish_seen_subcommand_from env' -l format -d 'Output format'
complete -c ccs -n '__fish_seen_subcommand_from env; and __fish_seen_argument -l format' -a 'openai anthropic raw' -d 'Format'
complete -c ccs -n '__fish_seen_subcommand_from env' -l shell -d 'Shell syntax'
complete -c ccs -n '__fish_seen_subcommand_from env; and __fish_seen_argument -l shell' -a 'bash fish powershell' -d 'Shell'
complete -c ccs -n '__fish_seen_subcommand_from env' -s h -l help -d 'Show help for env command'
# ============================================================================
# auth subcommands
# ============================================================================
+53 -1
View File
@@ -12,7 +12,7 @@
Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
param($commandName, $wordToComplete, $commandAst, $fakeBoundParameters)
$commands = @('auth', 'api', 'cliproxy', 'doctor', 'sync', 'update', '--help', '--version', '--shell-completion', '-h', '-v', '-sc')
$commands = @('auth', 'api', 'cliproxy', 'doctor', 'env', 'sync', 'update', '--help', '--version', '--shell-completion', '-h', '-v', '-sc')
$cliproxyProfiles = @('gemini', 'codex', 'agy', 'qwen')
$authCommands = @('create', 'list', 'show', 'remove', 'default', '--help', '-h')
$apiCommands = @('create', 'list', 'remove', '--help', '-h')
@@ -21,6 +21,9 @@ Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
$cliproxyCreateFlags = @('--provider', '--model', '--force', '--yes', '-y')
$providerFlags = @('--auth', '--config', '--logout', '--headless', '--help', '-h')
$updateFlags = @('--force', '--beta', '--dev', '--help', '-h')
$envFlags = @('--format', '--shell', '--help', '-h')
$envFormats = @('openai', 'anthropic', 'raw')
$envShells = @('bash', 'fish', 'powershell')
$shellCompletionFlags = @('--bash', '--zsh', '--fish', '--powershell')
$listFlags = @('--verbose', '--json')
$removeFlags = @('--yes', '-y')
@@ -130,6 +133,55 @@ Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
return
}
# env command completion
if ($words[1] -eq 'env') {
if ($position -eq 3) {
$options = $cliproxyProfiles + (Get-CcsProfiles -Type settings) + $envFlags
$options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new(
$_,
$_,
'ParameterValue',
$_
)
}
} elseif ($position -ge 4) {
switch ($words[$position - 2]) {
'--format' {
$envFormats | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new(
$_,
$_,
'ParameterValue',
$_
)
}
}
'--shell' {
$envShells | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new(
$_,
$_,
'ParameterValue',
$_
)
}
}
default {
$envFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new(
$_,
$_,
'ParameterValue',
$_
)
}
}
}
}
return
}
# auth subcommand completion
if ($words[1] -eq 'auth') {
if ($position -eq 3) {
+9 -1
View File
@@ -13,7 +13,7 @@
# sudo cp scripts/completion/ccs.zsh /usr/local/share/zsh/site-functions/_ccs
# Set up completion styles for better formatting and colors
zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|api|cliproxy|doctor|sync|update)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37'
zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|api|cliproxy|doctor|env|sync|update)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37'
zstyle ':completion:*:*:ccs:*:proxy-profiles' list-colors '=(#b)(gemini|codex|agy|qwen)([[:space:]]#--[[:space:]]#*)==0\;35=2\;37'
zstyle ':completion:*:*:ccs:*:model-profiles' list-colors '=(#b)(default|glm|glmt|kimi|[^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;32=2\;37'
zstyle ':completion:*:*:ccs:*:account-profiles' list-colors '=(#b)([^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;33=2\;37'
@@ -34,6 +34,7 @@ _ccs() {
'api:Manage API profiles (create/remove)'
'cliproxy:Manage CLIProxy variants and binary'
'doctor:Run health check and diagnostics'
'env:Export env vars for third-party tools'
'sync:Sync delegation commands and skills'
'update:Update CCS to latest version'
)
@@ -124,6 +125,13 @@ _ccs() {
_arguments \
'(- *)'{-h,--help}'[Show help for doctor command]'
;;
env)
_arguments \
'--format[Output format]:format:(openai anthropic raw)' \
'--shell[Shell syntax]:shell:(bash fish powershell)' \
'(- *)'{-h,--help}'[Show help]' \
'1:profile:($proxy_profiles)'
;;
gemini|codex|agy|qwen)
_arguments \
'--auth[Authenticate only]' \
+2 -1
View File
@@ -18,6 +18,7 @@ type OutputFormat = 'openai' | 'anthropic' | 'raw';
const VALID_FORMATS: OutputFormat[] = ['openai', 'anthropic', 'raw'];
const VALID_SHELLS: ShellType[] = ['bash', 'fish', 'powershell'];
const VALID_ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
/** Auto-detect shell from environment */
export function detectShell(flag?: string): ShellType {
@@ -231,7 +232,7 @@ export async function handleEnvCommand(args: string[]): Promise<void> {
// Output shell-formatted exports to stdout
for (const [key, value] of Object.entries(output)) {
if (value) {
if (value && VALID_ENV_KEY.test(key)) {
console.log(formatExportLine(shell, key, value));
}
}