Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Install the Grok CLI on Windows Without Pasting Your API Key into a Script

Install Bun if it's missing, install the open-source Grok CLI globally, and save your xAI API key as a user environment variable from a hidden prompt instead of a line in a script.

AT A GLANCEInstall-GrokCli.ps1
What it does
Installs Bun with its official installer if needed, installs or updates the open-source Grok CLI package globally with Bun (or npm), prompts for your xAI API key without echoing it and stores it as the GROK_API_KEY user environment variable, then checks the grok command runs.
Requires
  • Windows PowerShell 5.1 or PowerShell 7+
  • Internet access to bun.sh and the npm registry
  • An xAI API key
  • For -UseNpm, Node.js already installed
Permissions
A normal user account. Bun, the CLI and the environment variable all go in your own profile, so no admin rights are needed.
Runs on
Windows 10/11
Tested
Parse-checked and dry-run with -WhatIf in PowerShell 7.4 on Linux, with and without Bun on the PATH and with -UseNpm. The actual installs and the user environment variable are Windows-only and weren't run here.

Grok CLI is an open-source terminal agent for xAI's Grok models, in the same family as the other AI coding CLIs: it runs in your project folder, reads and edits files, and runs commands. Getting it going on Windows is three small jobs. Install Bun, install the package, give it your API key.

My original setup was a batch file, and like a lot of quick setup scripts it had the API key typed straight into it, then wrote it into a settings file too. That's the part worth fixing. This version asks for the key at a hidden prompt, stores it once as a user environment variable, and never takes it as a parameter, so it doesn't end up in your console history or in the script.

To be clear about what that buys you: an environment variable is still plain text in your user profile, readable by anything running as you. It's the normal place for a CLI key, and much better than a script you might share, but it isn't a vault.

Install-GrokCli.ps1Download
<#
.SYNOPSIS
    Installs the open-source Grok CLI with Bun and stores your xAI API key as a user environment variable.
.DESCRIPTION
    1. Checks for Bun and installs it with the official installer if it's missing (or uses npm with -UseNpm).
    2. Installs or updates the Grok CLI package globally (grok-dev by default; older guides call it @vibe-kit/grok-cli).
    3. Prompts for your xAI API key without echoing it, and saves it as the GROK_API_KEY user environment
       variable. It's never taken as a parameter, so it doesn't end up in your console history.
    4. Checks that the grok command runs and returns a summary object.

    Like any environment variable, the key is stored in plain text in your user profile (HKCU\Environment).
    Anything running as you can read it. Supports -WhatIf.
.PARAMETER PackageName
    The npm package to install. Default: grok-dev.
.PARAMETER UseNpm
    Install with npm instead of Bun. Needs Node.js already installed.
.PARAMETER SkipApiKey
    Install the CLI only and leave GROK_API_KEY alone.
.PARAMETER ReplaceApiKey
    Prompt for a new key even if GROK_API_KEY is already set.
.EXAMPLE
    .\Install-GrokCli.ps1
.EXAMPLE
    .\Install-GrokCli.ps1 -ReplaceApiKey
.EXAMPLE
    .\Install-GrokCli.ps1 -UseNpm -SkipApiKey -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [ValidatePattern('^(@[a-z0-9._-]+/)?[a-z0-9._-]+(@[\w.^~-]+)?$')][string]$PackageName = 'grok-dev',
    [switch]$UseNpm,
    [switch]$SkipApiKey,
    [switch]$ReplaceApiKey
)

$ErrorActionPreference = 'Stop'
$summary = [ordered]@{ Installer = $null; InstallerVersion = $null; Package = $PackageName; PackageStatus = 'Skipped'; GrokPath = $null; GrokVersion = $null; ApiKey = 'Unchanged' }
$bunBin = Join-Path $HOME '.bun\bin'

function Get-ToolVersion([string]$Name) {
    $cmd = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if (-not $cmd) { return $null }
    try { (& $cmd.Source --version 2>$null | Select-Object -First 1) } catch { $null }
}

# 1. Bun (or npm).
if ($UseNpm) {
    $summary.Installer = 'npm'
    $summary.InstallerVersion = Get-ToolVersion 'npm'
    if (-not $summary.InstallerVersion) { throw 'npm not found. Install Node.js LTS first, or run without -UseNpm to use Bun.' }
}
else {
    $summary.Installer = 'bun'
    if (-not (Get-Command bun -ErrorAction SilentlyContinue) -and (Test-Path -LiteralPath (Join-Path $bunBin 'bun.exe'))) {
        $env:Path = "$env:Path;$bunBin"   # installed earlier, but this session hasn't picked up the PATH change yet
    }
    $summary.InstallerVersion = Get-ToolVersion 'bun'
    if (-not $summary.InstallerVersion -and $PSCmdlet.ShouldProcess('Bun', 'Install with the official installer from bun.sh')) {
        $installer = Join-Path ([IO.Path]::GetTempPath()) "bun-install-$([guid]::NewGuid().ToString('N')).ps1"
        try {
            Invoke-WebRequest -Uri 'https://bun.sh/install.ps1' -OutFile $installer -UseBasicParsing
            & $installer
        }
        finally { Remove-Item -LiteralPath $installer -ErrorAction SilentlyContinue }
        if (-not (Get-Command bun -ErrorAction SilentlyContinue)) { $env:Path = "$env:Path;$bunBin" }
        $summary.InstallerVersion = Get-ToolVersion 'bun'
        if (-not $summary.InstallerVersion) { throw 'Bun install finished but bun still is not on PATH. Open a new terminal and run this again, or install Bun from bun.sh.' }
    }
}

# 2. The CLI itself.
if ($PSCmdlet.ShouldProcess($PackageName, "Install globally with $($summary.Installer)")) {
    if ($UseNpm) { & npm install -g $PackageName } else { & bun add -g $PackageName }
    if ($LASTEXITCODE -ne 0) { throw "$($summary.Installer) exited with code $LASTEXITCODE while installing $PackageName." }
    $summary.PackageStatus = 'Installed'
    if (-not $UseNpm -and ($env:Path -split ';') -notcontains $bunBin) { $env:Path = "$env:Path;$bunBin" }
}

# 3. The API key: prompted, never a parameter, never echoed.
if (-not $SkipApiKey) {
    $existing = [Environment]::GetEnvironmentVariable('GROK_API_KEY', 'User')
    if ($existing -and -not $ReplaceApiKey) {
        $summary.ApiKey = 'AlreadySet'
        Write-Verbose 'GROK_API_KEY is already set for this user. Use -ReplaceApiKey to change it.'
    }
    elseif ($PSCmdlet.ShouldProcess('GROK_API_KEY (user environment variable)', 'Set from a secure prompt')) {
        $secure = Read-Host -Prompt 'Paste your xAI API key (input is hidden)' -AsSecureString
        $plain = [Net.NetworkCredential]::new('', $secure).Password
        try {
            if ([string]::IsNullOrWhiteSpace($plain)) { throw 'No key entered. Nothing was changed.' }
            [Environment]::SetEnvironmentVariable('GROK_API_KEY', $plain.Trim(), 'User')
            $env:GROK_API_KEY = $plain.Trim()
            $summary.ApiKey = if ($existing) { 'Replaced' } else { 'Set' }
        }
        finally { $plain = $null; $secure.Dispose() }
    }
}

# 4. Does it run?
$grok = Get-Command grok -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if ($grok) {
    $summary.GrokPath = $grok.Source
    $summary.GrokVersion = Get-ToolVersion 'grok'
}
elseif ($summary.PackageStatus -eq 'Installed') {
    Write-Warning "grok isn't on PATH in this session yet. Open a new terminal and run: grok --version"
}

[pscustomobject]$summary

Parameters

ParameterTypeDefaultWhat it's for
-PackageNamestringgrok-devThe npm package to install. Older guides use @vibe-kit/grok-cli; grok-dev is the current name.
-UseNpmswitch—Install with npm instead of Bun. Node.js has to be installed already.
-SkipApiKeyswitch—Install or update the CLI only, and leave GROK_API_KEY alone.
-ReplaceApiKeyswitch—Prompt for a new key even if GROK_API_KEY is already set. Use it when you rotate the key.

Run it

First-time setup. You'll be asked for the key once.

.\Install-GrokCli.ps1

See what it would do without installing anything.

.\Install-GrokCli.ps1 -WhatIf

Update the CLI later without touching the key.

.\Install-GrokCli.ps1 -SkipApiKey

Rotate the key after revoking the old one in the xAI console (it updates the CLI while it's there).

.\Install-GrokCli.ps1 -ReplaceApiKey

What you'll see

Example outputvalues are illustrative
bun add v1.3.13
installed [email protected] with binaries:
 - grok
Paste your xAI API key (input is hidden): ********

Installer        : bun
InstallerVersion : 1.3.13
Package          : grok-dev
PackageStatus    : Installed
GrokPath         : C:\Users\jdoe\.bun\bin\grok.exe
GrokVersion      : 1.1.7
ApiKey           : Set

How it works

  1. Find or install Bun. It checks the PATH, then %USERPROFILE%\.bun\bin in case Bun was installed but this session hasn't picked it up yet. If there's no Bun, it downloads Bun's official install.ps1 to a temp file, runs it and cleans up. With -UseNpm it checks for npm instead and stops with a clear message if Node isn't installed.
  2. Install the CLI. bun add -g grok-dev (or npm install -g) installs or updates it, and a non-zero exit code stops the script instead of carrying on half-installed.
  3. Ask for the key. If GROK_API_KEY is already set for your user, it's left alone unless you pass -ReplaceApiKey. Otherwise Read-Host -AsSecureString prompts with the input hidden, and the key is saved with [Environment]::SetEnvironmentVariable(..., 'User') and set for the current session. An empty answer changes nothing.
  4. Check it runs. It looks for grok on the PATH and asks for its version, then returns a summary object. -WhatIf walks through every step without installing or prompting.

Take it further

  • Keep keys out of prompts, too. Storing the key well is half of it; the other half is not pasting secrets into the conversation. I wrote about that in Keeping Secrets Out of Prompts.
  • Run more than one agent. If you end up with several AI CLIs, how I build with three Claude Code agents at once covers keeping them out of each other's way.
  • Pin a model. The CLI also reads GROK_MODEL. Set it the same way, with [Environment]::SetEnvironmentVariable('GROK_MODEL', '<model>', 'User'), if you always use the same one.

Things that'll trip you up

  • New terminals only. Windows hands environment variables to programs when they start. The script sets GROK_API_KEY for its own session too, but terminals and editors that were already open won't see it until you restart them.
  • The key is plain text in your profile. User environment variables live in HKCU\Environment. Anything running as you can read them, and so can anyone with admin rights on the machine. Don't do this on a shared or untrusted PC, and revoke the key in the xAI console if the machine is ever lost.
  • One key, one place. If an older setup wrote an apiKey into %USERPROFILE%\.grok\user-settings.json, take it out, so there's only one copy to rotate and no stale key hiding in a JSON file.
  • The Bun installer runs from the internet. The script downloads bun.sh/install.ps1 and runs it, which is Bun's documented install method. If that's not allowed where you are, install Bun with winget install Oven-sh.Bun or use -UseNpm, then run the script again.