Part 1 of 4
Portable opencode installation on external SSD
Set up opencode 100% portable on J: with direct binary and launchers that isolate config, data, and cache without touching the host.
I wanted to use opencode on multiple PCs without repeating installations or leaving traces on each machine. The solution: put everything (binary, config, sessions, cache) on my external SSD J: and launch it with a script that sets portable environment variables.
What we need
- External drive mounted at
J:(exFAT or NTFS, never FAT32 due to the 4 GB file limit) - Node.js ≥ 20 on the PC where we do the installation (only for downloading; the opencode binary doesn’t need it)
- PowerShell 5.1+
Note: opencode on native Windows (
cmd.exe) has a known bug: exiting with Ctrl+D or/exitcloses the entire terminal window. The portable installation we’re building doesn’t fix this — it still happens if you launch fromcmd.exe. Recommended practice: use it inside Windows Terminal or Git Bash, or better yet, from WSL2. In this tutorial we stay on native Windows because the goal is portability between Windows machines.
1. Directory structure on the SSD
New-Item -ItemType Directory -Path "J:\opencode\bin" -Force
New-Item -ItemType Directory -Path "J:\opencode\config" -Force
New-Item -ItemType Directory -Path "J:\opencode\data" -Force
New-Item -ItemType Directory -Path "J:\opencode\cache" -Force
New-Item -ItemType Directory -Path "J:\opencode\logs" -Force
New-Item -ItemType Directory -Path "J:\opencode\state" -Force
Result:
J:\
├── opencode.ps1 ← PowerShell launcher
├── opencode.bat ← CMD launcher
└── opencode\
├── bin\opencode.exe
├── config\
├── data\
├── cache\
├── logs\
└── state\
2. Download the opencode binary (Windows x64)
The cleanest way is to grab the CLI ZIP from GitHub Releases and extract only the .exe:
# Get latest version
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/anomalyco/opencode/releases/latest" -ErrorAction Stop
$asset = $release.assets | Where-Object { $_.name -eq "opencode-windows-x64.zip" } | Select-Object -First 1
$asset.browser_download_url
# → https://github.com/anomalyco/opencode/releases/download/v1.18.10/opencode-windows-x64.zip
# Download and extract
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile "J:\opencode\opencode-windows-x64.zip"
Expand-Archive -LiteralPath "J:\opencode\opencode-windows-x64.zip" -DestinationPath "J:\opencode\bin" -Force
Remove-Item "J:\opencode\opencode-windows-x64.zip" -Force
# Verify
& "J:\opencode\bin\opencode.exe" --version
# 1.18.10
Why not
npm install -g: it installs to%APPDATA%\npm(local user path) and pulls hundreds of dependencies. The direct binary is ~170 MB, zero dependencies, and lives on the SSD.
3. Portable launchers (the trick)
Create two files at J:\ root so double-clicking/running them starts opencode with all variables pointing to the SSD:
J:\opencode.ps1 (PowerShell, recommended):
param(
[switch]$Console,
[switch]$StopOmniRoute
)
$drive = "J:"
$base = "$drive\opencode"
$nodeDir = "$drive\node\dist\node-v22.23.2-win-x64"
$omniBin = "$drive\omniroute\node_modules\omniroute\bin\omniroute.mjs"
$omniWork = "$drive\omniroute"
$env:XDG_DATA_HOME = "$base\xdg\share"
$env:XDG_CONFIG_HOME = "$base\xdg\config"
$env:XDG_CACHE_HOME = "$base\xdg\cache"
$env:XDG_STATE_HOME = "$base\xdg\state"
$env:PATH = "$nodeDir;$base\bin;$env:PATH"
$env:DATA_DIR = "$omniWork\data"
$script:omniStartedByUs = $false
function Test-OmniRouteUp {
try {
$r = Invoke-WebRequest -Uri "http://localhost:20128/api/monitoring/health" -UseBasicParsing -TimeoutSec 3 -ErrorAction Stop
return $r.StatusCode -eq 200
} catch {
return $false
}
}
function Start-OmniRoute {
if (Test-OmniRouteUp) {
Write-Host "OmniRoute is running on (localhost:20128)."
return
}
Write-Host "Launching OmniRoute..."
$script:omniStartedByUs = $true
Start-Process -FilePath "$nodeDir\node.exe" -ArgumentList "`"$omniBin`"","serve" -WorkingDirectory $omniWork -WindowStyle Hidden
$ok = $false
for ($i = 0; $i -lt 60; $i++) {
Start-Sleep -Seconds 2
if (Test-OmniRouteUp) { $ok = $true; break }
}
if ($ok) {
Write-Host "OmniRoute ready (localhost:20128)."
} else {
Write-Warning "OmniRoute did not respond. Opening opencode anyway."
}
}
if ($StopOmniRoute) {
try {
Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like "*omniroute*" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
Write-Host "OOmniRoute stopped."
} catch {
Write-Warning "Failed to stop OmniRoute: $($_.Exception.Message)"
}
exit
}
Start-OmniRoute
if ($Console) {
& "$base\bin\opencode.exe" --no-tui @args
} else {
& "$base\bin\opencode.exe" @args
}
if ($script:omniStartedByUs) {
Write-Host ""
Write-Host "Opencode finalized."
$answer = Read-Host "The OmniRoute server was started by this script. Do you want to shut it down? (Y/N)"
if ($answer -match "^[yY]") {
try {
Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like "*omniroute*" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
Write-Host "OOmniRoute stopped."
} catch {
Write-Warning "Failed to stop OmniRoute: $($_.Exception.Message)"
}
} else {
Write-Host "OmniRoute is still running on localhost:20128."
}
}
4. Test it works
From any terminal (PowerShell, CMD, Windows Terminal):
J:\opencode.ps1 --version
# 1.18.10
J:\opencode.ps1 --help
# Shows commands: run, attach, acp, mcp, debug, providers...
If you see the version and help, you’re ready. Plug the SSD into another Windows, open a terminal, run J:\opencode.ps1 — you’ll have your opencode with your config, sessions, and history, leaving nothing on the host.
Issues and lessons
Lesson:
dubious ownershipin Git
If your project repo is also on the SSD (J:\Codigo\my-project), Git complains about dubious ownership. Fix:git config --global --add safe.directory "J:/Codigo/my-project"
Lesson: noreply email on public repos
To keep your personal email out of commit history:git config user.email "12345678+your-user@users.noreply.github.com"
Something we learned the hard way: the
cmd.exebug
In nativecmd.exe, exiting opencode (Ctrl+D,/exit,/quit) closes the terminal window. This isn’t our portable install’s fault, it’s an opencode bug on Windows (#22003). Workarounds:
- Use Windows Terminal (not raw
cmd.exe)- Use Git Bash or WSL2
- If you must use
cmd.exe, launchopencode --no-tuiand exit with Ctrl+C
What’s next
Now you have portable opencode. In the next part we integrate OmniRoute + best-free-coding combo for automatic fallback between free models (Nemotron 3 Ultra, DeepSeek V4 Flash, Big Pickle).