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:
- OpenCode (ofcourse)
- Node Version Manager (NVM) for Windows, to help us install skills.
- CoreUtils, which is a port of common GNU utils (like sed, ls, etc) for Windows. LLMs love using those.
- 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:
| MCP | What it does | API key | Where to get it |
|---|---|---|---|
| Context7 | Provides current documentation and code examples for libraries and frameworks. | Optional | Create a free account at context7.com/dashboard. The API key provides higher rate limits. |
| Playwright MCP | Allows OpenCode to control a browser for navigation, testing, screenshots, and page inspection. | Not required | It runs locally through npx. The required browser dependencies are downloaded when the server starts. |
| AWS Knowledge MCP | Provides current AWS documentation, API references, best practices, regional availability, and AWS code examples. | Not required | The 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.