Run opencode as a truly portable app on Windows 10 / 11: double-click, pick a folder, work.
Close it — quit, X, Ctrl+C, even Task Manager kill — and the app removes its own files (verified with the checklist below; scope note on OS telemetry).
Designed for opencode's built-in free models — no login, no providers to connect. External providers (NVIDIA, Anthropic, OpenAI, …) were not tested with this project: API keys are accepted per session but provider setups are unverified.
Tested with opencode
1.18.31(snapshot version; setup scripts default to the pinnedUPSTREAM_VERSION).
No install, no setup: grab OpencodePortable.exe from the
latest release,
put it anywhere (Desktop, USB stick), double-click it. That's the whole
installation.
Verify the download against the published .sha256 (attached next to the
exe in every release, alongside the CycloneDX SBOM OpencodePortable.exe.cdx.json):
if ((Get-FileHash OpencodePortable.exe -Algorithm SHA256).Hash.ToLower() -ne (Get-Content OpencodePortable.exe.sha256)) { throw 'hash mismatch' }- Double-click
OpencodePortable.exe. - Pick the work folder (explorer dialog, or drag-and-drop it into the console).
- Work. On close, everything the app wrote — config, cache, state, tmp, binary — is deleted with the per-run folder. Nothing is ever written next to the exe.
| Concern | What you get |
|---|---|
| Isolation | Config, data, cache, state and temp live in <workspace>/.opencode-portable-<timestamp>-<pid>-<rand>/, deleted on exit — normal quit, X/Alt+F4, Ctrl+C and Task Manager kill included |
| Offline | Embedded opencode binary, extracted on first run — no network, ever |
| Fast startup | Skips the blocking 10–30 s models.dev fetch |
| No login stored | Built-in free models just work; provider API keys are typed per session, never stored |
| Concurrent runs | Each run gets its own folder — no sharing, no locks |
After a run, check nothing was left behind:
:: 1. workspace clean (shows hidden/system too):
dir "C:\path\to\workspace" /a
:: 2. known leftovers on C: (the exe itself is expected):
dir C:\*opencode* /s /b /a
dir C:\opwatch-*.exe /s /b /a
:: 3. no lingering processes (tasklist takes no wildcards):
tasklist | findstr /I "opencode opwatch"Expected: only your files, only the exe, no processes.
Build from source
git clone https://github.com/terzastella/opencode-portable.git
cd opencode-portable# All-in-one exe (requires .NET 10 SDK) — resolves latest opencode,
# embeds + hashes it, publishes to dist/:
.\scripts\publish-fat.ps1See docs/HOW-IT-WORKS.md for the full architecture.
Option B — scripts (Windows .cmd/.ps1, Linux/macOS .sh)
Download the binary for your OS into bin/ (binaries are not committed to git):
# Windows
.\scripts\setup.ps1
# optionally: .\scripts\setup.ps1 -Version 1.18.31# Linux / macOS
./scripts/setup.sh
# optionally: ./scripts/setup.sh --version 1.18.31Run it:
:: Windows (double-click works too)
opencode-portable.cmd --help# Windows PowerShell (pipeline-capable)
.\opencode-portable.ps1 --help# Linux / macOS
./opencode-portable.sh --helpOn first run config/opencode.json is auto-created from config/opencode.example.json.
(--help is opencode's own flag: it needs the downloaded binary, otherwise launchers fail closed.)
Project structure
.
├── dist/OpencodePortable.exe # single-file Windows app (build: scripts/publish-fat.ps1)
├── opencode-portable.cmd # Windows launcher (double-click friendly)
├── opencode-portable.ps1 # Windows PowerShell launcher (mirror)
├── opencode-portable.sh # Linux / macOS launcher (mirror)
├── bin/ # binaries go here (git-ignored, see scripts/setup.*)
├── assets/ # logo.svg, logo.png, opencode-portable.ico
├── config/
│ ├── opencode.example.json # tracked template
│ └── opencode.json # your local config (git-ignored, auto-created)
├── data/ # all runtime files (git-ignored)
│ ├── config/ data/ cache/ state/ tmp/
├── scripts/
│ ├── setup.ps1 # download windows-x64/arm64 binary
│ ├── setup.sh # download linux/darwin x64/arm64 binary
│ ├── publish-fat.ps1 # build the all-in-one exe (embeds upstream zip)
│ ├── pack-release.ps1/.sh # clean release archives with secret scan
│ └── install-shortcuts.ps1 # Desktop + Start Menu shortcuts with icon
├── src/OpencodePortable/ # C# source of the single-file exe
├── docs/HOW-IT-WORKS.md # architecture details
├── .github/workflows/ # CI: smoke on push/PR, full payload on tags
├── UPSTREAM_VERSION # pinned upstream opencode version
├── global.json # pinned .NET SDK
├── THIRD-PARTY-NOTICES.md # components, versions, licenses
├── SECURITY.md # policy, scope, disclosure
├── CHANGELOG.md # release history
├── LICENSE # MIT
├── README.md # this file (EN)
└── README.it.md # Italian version
(bin/ holds opencode.exe + .gitkeep; config/ holds the tracked
opencode.example.json; dist/, data/, src/**/payload|bin|obj are
git-ignored build/runtime outputs.)
Desktop shortcuts (Windows):
.\scripts\install-shortcuts.ps1 # create Desktop + Start Menu shortcuts
.\scripts\install-shortcuts.ps1 -Uninstall # remove themShortcuts launch opencode-portable.cmd with the custom icon from assets/opencode-portable.ico. (Deliberately the script, not the exe: shortcuts are for quick terminal use; the exe is the double-click app. Windows-only script; validates its folders first.) Working directory is the portable root.
How it works
| Concern | What the launchers do |
|---|---|
| Isolation | Set XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME, XDG_STATE_HOME to data/..., and OPENCODE_CONFIG to config/opencode.json |
| Fast startup | Set OPENCODE_DISABLE_MODELS_FETCH=1 (skips the blocking 10–30 s models.dev fetch) |
| Temp | Isolated per-run tmp (TMP/TEMP/TMPDIR point inside it, deleted on exit). Naming differs per launcher: run-HHMMSScc-RAND (cmd, locale-sanitized), run-yyyyMMdd-HHmmss-PID-rand (ps1), mktemp run-YYYYMMDD-HHMMSS-PID-XXX (sh); the exe needs no sub-level (its root is already per-run) |
| Crash leftovers | Swept next run (exe + ps1 + sh: dead PID + exact stamp only, live never touched; cmd: age >7 days, no PID check — documented divergence) |
| Host pollution guard | Snapshot %TEMP%\opencode, %LOCALAPPDATA%\opencode, %APPDATA%\opencode (Win) or ~/.config/opencode, ~/.local/share/opencode, ~/.cache/opencode (Unix) and remove them on exit only if this run created them (exe also covers %USERPROFILE%\.config\opencode + .local\share\opencode) |
| Env hygiene | .ps1/.sh restore or scope env vars (try/finally, trap); concurrent instances are safe |
The exe adds: supervised child folder picker (native crash degrades to
drag-and-drop fallback), deletion retried against locked files (leftovers
reported, never silent), --clean <workspace> sweeper, and an orphan watchdog
(opwatch-<pid>-<rand>.exe, self-deleting) covering even Task Manager group
kills. Watchdog telemetry is written to %TEMP% only on failure/abort/leftovers, never on success.
See docs/HOW-IT-WORKS.md for details.
Binaries policy
bin/, data/, dist/, src/**/payload|bin|obj and local config/opencode.json are git-ignored. The repo ships source only:
scripts/setup.ps1→opencode-windows-x64.zip(or-arm64, auto-detected, WOW64-aware) from upstream releasesscripts/setup.sh→opencode-linux-x64.tar.gz/opencode-darwin-arm64.zip, etc. (fails fast on unsupported OS/arch)- If
bin/is empty the launchers refuse to run (fail-closed): reusingopencodefromPATHrequires explicit opt-in (--from-path— stripped anywhere by exe/cmd, first-arg-only by ps1/sh — orOPENCODE_ALLOW_PATH_FALLBACK=1), and the resolved path is always printed. The exe prefers a bundled binary and only then looks at PATH.
For distribution: pack-release.ps1 builds opencode-portable-win-x64.zip, pack-release.sh builds opencode-portable-linux-macos.tar.gz. Both secret-scan the content and never include data/, bin/* (except .gitkeep), dist/ or local configs. The all-in-one exe itself (dist/) is never committed (~137 MiB / ~144 MB).
Security notes
- Binaries are trusted upstream artifacts:
setup.*downloads andpublish-fat.ps1embeds over HTTPS from GitHub releases. The embedded payload hash (SHA-256) is pinned at build time and re-verified before every extraction; setup scripts default to the pinnedUPSTREAM_VERSION(explicit-Versionwins). Missing piece (no unilateral fix): upstream publishes no checksums/signatures, so there is no independent trust anchor — if they ever do, verify against them. - Credentials live in
data/(scripts) or<workspace>/.opencode-portable-*/(exe) (auth tokens, API keys): never share or publish those folders, and never ship an archive containing them —pack-releaseexcludes them by design. A lost USB stick means lost credentials. - Downloaded zips carry Mark-of-the-Web: on other PCs Windows may block the
.ps1launchers (execution policy). Useopencode-portable.cmd, orUnblock-File, after inspecting the content. - This wrapper isolates files, it does not sandbox opencode itself: an AI coding agent runs shell commands in your workspace — review what it does, as with any upstream install.
- Trust model: the workspace is NOT a security boundary — use only folders you trust (no network shares, synced folders writable by others, or untrusted repos); anyone who can write the workspace can influence config, cache and extracted binaries. Same for
--from-path/OPENCODE_ALLOW_PATH_FALLBACK=1: it bypasses the bundled-payload trust and runs whateveropencodeis first in PATH — resolved path is always printed, hash it yourself if unsure. - Updates: no auto-update (
autoupdate: false); each release pins its opencode version inUPSTREAM_VERSIONat build time (publish-fatuses the file unless-Versionoverrides it, then embeds + hashes it). To update, bump the file and download the new release. - CI: every push/PR builds on Windows and runs the self-test suite (status badge above; tags run an extra full-payload
fatjob). - No Windows Error Reporting: the exe silences WER for its own crashes (including supervised picker-child AVs) so nothing lands in
ReportArchive; diagnostics stay in our owncrash-*.log(<root>/state/normally,%TEMP%\opencode-portable-crash-*/only if crashing before any workspace is known; last 5 kept, usernames scrubbed).--clean-hostremoves our host-side traces (old WER archives, watchdog telemetry leftovers, TEMP crash dir) — pre-existing archives may need admin. - "Zero traces" scope: this removes application-created files and known runtime artifacts (verified with the checklist below). It is not forensic anti-tracking: OS/security telemetry outside our control (Prefetch, Defender/SmartScreen telemetry, USN Journal, pagefile, shell history, antivirus logs) cannot be removed by any portable app.
Exe flags & exit codes
| Flag | Effect |
|---|---|
--workspace <dir> / --workspace=<dir> |
Skip the picker, start directly there |
--console |
Skip the dialog, go straight to drag-and-drop input |
--from-path |
Opt in to reusing opencode from PATH if no binary found (anywhere pre---; also OPENCODE_ALLOW_PATH_FALLBACK=1) |
--picker-exe <path> / --picker-exe=<path> |
Use another executable as the picker child (diagnostics) |
--pick-folder [title] [initial] |
Child mode: native dialog, stdout protocol PICK-OK (0) / PICK-CANCEL (1) / PICK-ERROR (2) |
--watch <pid> <ticks> <dir> |
Watchdog mode (spawned automatically, orphan hop) |
--watch-hop <pid> <ticks> <dir> |
Internal hop: spawns the detached --watch grandchild and exits (do not call directly) |
--clean <dir> |
Sweep stale roots without launching; errors on missing dir |
--clean-host |
Remove own host traces (WER archives, watchdog telemetry, TEMP crash dir); always exit 0 |
--self-test |
Non-UI COM smoke test only (instantiate + options + WER-silenced check, never Show — needs no human click) |
--test-fallback, --test-close, --test-robust-delete, --test-watchdog, --test-watchdog-proc, --test-watchdog-copy, --test-watchdog-hop, --test-detached, --test-junction, --test-midrun-delete, --test-race, --test-longpath |
Built-in self-tests, gated behind OPENCODE_ENABLE_TESTFLAGS=1 (see source) |
--report-console <file> |
Detachment probe, gated like the test flags above |
--has-payload |
Report embedded payload info; exit 0 iff present (used by publish-fat) |
-- |
Everything after is forwarded to opencode verbatim, even launcher-like flags |
Exit codes: 0 = ok / picker cancelled / --clean-host done / --has-payload present; 1 = invalid workspace (incl. drive root), missing binary/config, failed fallback, failed self-test, --clean missing dir, --watch/--watch-hop/--picker-exe/--report-console misuse, watchdog abort, fatal error (MessageBox + crash-*.log with line numbers); 2 = --pick-folder child internal error only (the parent auto-falls-back, never exits 2 in a normal run); otherwise opencode's own exit code is propagated.
Config
Scripts flow: edit config/opencode.json (defaults: autoupdate: false, share: "disabled" — sensible for a portable install). The template lives in config/opencode.example.json; delete your local opencode.json to reset to defaults.
Exe flow: the config is ephemeral per run (<root>/config/opencode.json, from the template next to the exe or the embedded default) and deleted on exit with everything else.
Note: this project targets opencode's built-in free models only — no login, no external providers to connect. Login/auth state never persists for the exe (fresh root every run); provider API keys, if you ever need other models, are typed per session, never stored (see HOW-IT-WORKS § Auth note).
Full architecture: docs/HOW-IT-WORKS.md (English).
MIT — © 2026 Terzastella.
