Installing and Configuring OpenCode on Windows

If you want to work with AI and large language models, you'll need a coding-agent harness. My weapon of choice is OpenCode on Windows. This article will guide you through installing and configuring the tool itself, AI providers, MCPs, and skills.

Install

We need to install some software:

  1. OpenCode (ofcourse)
  2. Node Version Manager (NVM) for Windows, to help us install skills.
  3. CoreUtils, which is a port of common GNU utils (like sed, ls, etc) for Windows. LLMs love using those.
  4. Edikt for editting the JSON file.

Edikt is a young project. This guide pins version v0.5.0; review newer releases before updating it.

Let's install:

& {
  function Update-Path {
    $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine")
    $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
    $env:Path = "$machinePath;$userPath"
  }

  # Install OpenCode, NVM for Windows, and Coreutils.
  winget install --id SST.opencode
  winget install --id CoreyButler.NVMforWindows
  winget install --id Microsoft.Coreutils

  # Download the pinned edikt release for lossless JSONC edits.
  $ediktVersion = "v0.5.0"
  $ediktDir = Join-Path $env:LOCALAPPDATA "edikt"
  $ediktPath = Join-Path $ediktDir "edikt.exe"
  $ediktZip = Join-Path $env:TEMP "edikt-$ediktVersion.zip"
  New-Item -ItemType Directory -Force -Path $ediktDir | Out-Null
  Invoke-WebRequest "https://github.com/jhheider/edikt/releases/download/$ediktVersion/edikt-windows-x86_64.zip" -OutFile $ediktZip
  Expand-Archive -Force -Path $ediktZip -DestinationPath $ediktDir
  Remove-Item $ediktZip
  Update-Path
  Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
  nvm install lts
  nvm use lts
  Start-Sleep -Seconds 1
  Update-Path
  Write-Host "opencode: $(opencode --version)"
  Write-Host "node: $(node --version)"
  Write-Host "npm: $(npm --version)"
}

Providers

For API-key providers, run the following command and select the provider interactively:

opencode auth login

For GitHub Copilot, start OpenCode and run /connect, then select GitHub Copilot. See the OpenCode provider documentation for the complete authentication flow.

MCPs

In this section, we configure three MCP servers:

MCPWhat it doesAPI keyWhere to get it
Context7Provides current documentation and code examples for libraries and frameworks.OptionalCreate a free account at context7.com/dashboard. The API key provides higher rate limits.
Playwright MCPAllows OpenCode to control a browser for navigation, testing, screenshots, and page inspection.Not requiredIt runs locally through npx. The required browser dependencies are downloaded when the server starts.
AWS Knowledge MCPProvides current AWS documentation, API references, best practices, regional availability, and AWS code examples.Not requiredThe managed AWS endpoint is public and does not require an AWS account or AWS credentials. It is subject to rate limits.

Let's go:

& {
  $configPath = @(Join-Path $HOME ".config\opencode\opencode.jsonc"; Join-Path $HOME ".config\opencode\opencode.json") | ? { Test-Path $_ } | Select-Object -First 1
  $backupPath = "$configPath.backup.$(Get-Date -Format 'yyyyMMdd_HHmmss_fff')"
  Copy-Item $configPath $backupPath
  Write-Host "Backup created at $backupPath"
  $ediktPath = Join-Path $env:LOCALAPPDATA "edikt\edikt.exe"
  $configDir = Split-Path $configPath
  $secretDir = Join-Path $configDir "secrets"
  $keyPath = Join-Path $secretDir "context7-api-key"
  New-Item -ItemType Directory -Force -Path $configDir, $secretDir | Out-Null
  if (-not (Test-Path -LiteralPath $keyPath) -or [string]::IsNullOrWhiteSpace((Get-Content -Raw -LiteralPath $keyPath))) {
    New-Item -ItemType File -Force -Path $keyPath | Out-Null
    Start-Process notepad.exe -ArgumentList "`"$keyPath`"" -Wait
    if ([string]::IsNullOrWhiteSpace((Get-Content -Raw -LiteralPath $keyPath))) { throw "The Context7 API key file is empty." }
  } else { Write-Host "Context7 API key already exists; leaving it unchanged." }
  $ediktScript = Join-Path $env:TEMP "opencode-$([guid]::NewGuid()).edk"
  $success = $false
  try {
    @'
.mcp.context7 = {type: "remote", url: "https://mcp.context7.com/mcp", headers: {CONTEXT7_API_KEY: "{file:secrets/context7-api-key}"}} |
.mcp.playwright = {type: "local", command: ["npx", "-y", "@playwright/mcp@latest"]} |
.mcp."aws-knowledge" = {type: "remote", url: "https://knowledge-mcp.global.api.aws"}
'@ | Set-Content -LiteralPath $ediktScript -Encoding utf8
    $output = & $ediktPath -t jsonc -f $ediktScript --in-place -- $configPath 2>&1
    if ($LASTEXITCODE) { throw "edikt failed: $(($output -join ' ') -replace '\s+', ' ')" }
    & opencode debug config *> $null
    if ($LASTEXITCODE) { throw "OpenCode rejected the updated configuration." }
    $success = $true
  } finally { Remove-Item $ediktScript -Force -ErrorAction SilentlyContinue; if (-not $success) { Copy-Item $backupPath $configPath -Force; Write-Warning "The update failed; the backup was restored." } }
  Write-Host "OpenCode configuration updated and validated at $configPath"
  Write-Host "Restart OpenCode to load the new configuration."
}

Protect Configuration Files

The following script protects environment and application configuration files. Example files remain readable.

& {
  $configPath = @(Join-Path $HOME ".config\opencode\opencode.jsonc"; Join-Path $HOME ".config\opencode\opencode.json") | ? { Test-Path $_ } | Select-Object -First 1
  $backupPath = "$configPath.backup.$(Get-Date -Format 'yyyyMMdd_HHmmss_fff')"
  Copy-Item $configPath $backupPath
  Write-Host "Backup created at $backupPath"
  $ediktPath = Join-Path $env:LOCALAPPDATA "edikt\edikt.exe"
  $ediktScript = Join-Path $env:TEMP "opencode-$([guid]::NewGuid()).edk"
  $success = $false
  try {
    $expressions = @('.permission.read[".env*"] = "deny"'; '.permission.read["appsettings*.json"] = "deny"'; '.permission.read["*.example*"] = "allow"')
    ($expressions -join " |`n") | Set-Content -LiteralPath $ediktScript -Encoding utf8
    $output = & $ediktPath -t jsonc -f $ediktScript --in-place -- $configPath 2>&1
    if ($LASTEXITCODE) { throw "edikt failed: $(($output -join ' ') -replace '\s+', ' ')" }
    & opencode debug config *> $null
    if ($LASTEXITCODE) { throw "OpenCode rejected the updated configuration." }
    $success = $true
  } finally { Remove-Item $ediktScript -Force -ErrorAction SilentlyContinue; if (-not $success) { Copy-Item $backupPath $configPath -Force; Write-Warning "The update failed; the backup was restored." } }
  Write-Host "Configuration file permissions updated and validated at $configPath"
}

OpenCode allows shell commands by default. This optional script adds confirmation prompts for potentially destructive commands while leaving other commands at their defaults.

Ask Before Commands

& {
  $configPath = @(Join-Path $HOME ".config\opencode\opencode.jsonc"; Join-Path $HOME ".config\opencode\opencode.json") | ? { Test-Path $_ } | Select-Object -First 1
  $backupPath = "$configPath.backup.$(Get-Date -Format 'yyyyMMdd_HHmmss_fff')"
  Copy-Item $configPath $backupPath
  Write-Host "Backup created at $backupPath"
  $ediktPath = Join-Path $env:LOCALAPPDATA "edikt\edikt.exe"
  $askCommands = @(
    "curl *localhost*"
    "curl *127.0.0.1*"
    "curl *0.0.0.0*"
    "curl * -X POST*"
    "curl * --request POST*"
    "curl * -X PUT*"
    "curl * --request PUT*"
    "curl * -X PATCH*"
    "curl * --request PATCH*"
    "curl * -X DELETE*"
    "curl * --request DELETE*"
    "curl * -d *"
    "curl * --data*"
    "git commit*"
    "git push*"
    "git branch -d*"
    "git branch -D*"
    "git merge*"
    "git rebase*"
    "git reset --hard*"
    "git cherry-pick*"
    "git tag -d*"
    "git remote remove*"
    "git clean*"
    "terraform apply*"
    "terraform destroy*"
    "terraform state *"
    "terraform import*"
    "terraform taint*"
    "terraform untaint*"
    "tofu apply*"
    "tofu destroy*"
    "tofu state *"
    "tofu import*"
    "tofu taint*"
    "tofu untaint*"
    "ansible-playbook*"
    "aws *"
    "gcloud *"
    "az *"
    "doctl *"
    "kubectl *"
    "helm *"
    "docker *"
    "docker-compose *"
    "psql*"
    "mysql*"
    "mongosh*"
    "redis-cli*"
    "iptables*"
    "ufw*"
    "firewall-cmd*"
    "apt-get *"
    "apt *"
    "yum *"
    "dnf *"
    "brew *"
    "snap *"
    "rm *"
    "rm -rf*"
    "rmdir*"
    "mv *"
    "dd *"
    "mkfs*"
    "fdisk*"
    "parted*"
    "chmod*"
    "chown*"
    "sudo *"
    "systemctl *"
    "reboot*"
    "shutdown*"
    "kill*"
    "pkill*"
    "ssh *"
    "scp *"
    "npm *"
    "npx *"
    "yarn *"
    "pnpm *"
    "bun *"
    "powershell *"
    "pwsh *"
    "Remove-Item*"
    "Move-Item*"
    "Copy-Item*"
    "Rename-Item*"
    "Set-Content*"
    "Add-Content*"
    "Out-File*"
    "Clear-Content*"
    "New-Item*"
    "Invoke-WebRequest*"
    "Invoke-RestMethod*"
    "Start-Process*"
    "Stop-Process*"
    "Set-ExecutionPolicy*"
    "Stop-Computer*"
    "Restart-Computer*"
    "Stop-Service*"
    "Restart-Service*"
    "Remove-Service*"
    "winget *"
  )
  $ediktScript = Join-Path $env:TEMP "opencode-$([guid]::NewGuid()).edk"
  $success = $false
  try {
    $expressions = @('.permission.external_directory = "ask"', '.permission.doom_loop = "ask"')
    $expressions += $askCommands | ForEach-Object { '.permission.bash["' + $_ + '"] = "ask"' }
    ($expressions -join " |`n") | Set-Content -LiteralPath $ediktScript -Encoding utf8
    $output = & $ediktPath -t jsonc -f $ediktScript --in-place -- $configPath 2>&1
    if ($LASTEXITCODE) { throw "edikt failed: $(($output -join ' ') -replace '\s+', ' ')" }
    & opencode debug config *> $null
    if ($LASTEXITCODE) { throw "OpenCode rejected the updated configuration." }
    $success = $true
  } finally { Remove-Item $ediktScript -Force -ErrorAction SilentlyContinue; if (-not $success) { Copy-Item $backupPath $configPath -Force; Write-Warning "The update failed; the backup was restored." } }
  Write-Host "Command permissions updated and validated at $configPath"
}

Skills

Install the skills globally with the skills CLI:

& {
  npx skills add https://github.com/anthropics/skills --skill skill-creator -g
  npx skills add https://github.com/mindrally/skills --skill htmx -g
  npx skills add https://github.com/vercel-labs/skills --skill find-skills -g
  npx skills add https://github.com/github/awesome-copilot --skill git-commit -g
}

These commands use the skill sources listed on skills.sh. Review a skill before installing it, because skills are executable instructions for an AI agent.

expand_less brightness_auto