This fork is designed to run against sensitive internal source code. The local CostasCode harness remains intact, but application-owned networking is deny-by-default and hosted CostasCode/Anomaly services are not part of the shipped product.
| Surface | Technology | Security role |
|---|---|---|
| CLI and local server | Bun/TypeScript, Effect HTTP, Node HTTP listener | packages/opencode; owns CLI commands, sessions, tools, local MCP, PTY, provider composition, and the legacy HTTP API |
| Session core | Effect services and SQLite | packages/core; owns durable sessions, filesystem/location boundaries, credentials, projects, worktrees, and native LLM routing |
| Protocol/server/client | Effect HttpApi and generated clients | packages/protocol, packages/server, packages/client; client generation runs from packages/client |
| LLM protocol layer | Effect HTTP/WebSocket transports | packages/llm; provider-independent protocol and stream handling |
| Web application | SolidJS/Vite | packages/app; connects only to a loopback backend |
| Desktop | Electron 42 with sandboxed renderer | packages/desktop; loads oc://renderer, starts an authenticated loopback utility-process sidecar, and exposes a narrow preload API |
The cloud console, hosted web site, enterprise service, stats site, Slack app, and serverless function sources are intentionally excluded from the root Bun workspace. They are not installed, typechecked, built, packaged, or released by the workplace product.
The shared contract is @opencode-ai/core/outbound:
Outbound.classify(url)parses and classifies a destination.Outbound.authorize(url, transport, audit?)fails closed and emits a metadata-only audit event.Outbound.guardedFetch(...)disables automatic redirects, validates every redirect, permits same-origin redirects only, and caps redirect depth.Outbound.installGlobalPolicy(audit?)wraps process/browserfetchand the globalWebSocketconstructor.
The Node composition installs the policy from
packages/core/src/effect/app-node-platform.ts. The CLI installs it at process
startup in packages/opencode/src/index.ts. Legacy and V2 model resolution
also validate the configured provider endpoint before invoking SDKs, covering
clients that use their own HTTP/WebSocket implementation. Electron enforces the
same policy through session.webRequest in
packages/desktop/src/main/windows.ts.
The OpenAI-compatible ws transport performs its own authorization before
constructing the third-party WebSocket client. Ambient HTTP proxy variables are
not forwarded to that transport, so an approved destination cannot be silently
rerouted through an undocumented proxy.
| Destination | Protocol | Purpose |
|---|---|---|
Embedded data: resources |
Fetch only | In-memory image/media conversion without network access |
localhost, 127.0.0.0/8, [::1] |
HTTP(S), WS(S) | Local server, PTY, callback, local provider, and explicitly enabled local MCP traffic. External browser opens are restricted to HTTP(S). |
github.com |
HTTPS/WSS | GitHub device OAuth and user-opened repository/support links |
api.github.com |
HTTPS/WSS | GitHub API and GitHub Copilot token operations |
docs.github.com |
HTTPS/WSS | User-opened GitHub documentation |
api.githubcopilot.com |
HTTPS/WSS | GitHub Copilot models and inference |
api.individual.githubcopilot.com |
HTTPS/WSS | GitHub Copilot individual routing |
api.business.githubcopilot.com |
HTTPS/WSS | GitHub Copilot business routing |
api.enterprise.githubcopilot.com |
HTTPS/WSS | GitHub Copilot enterprise-plan routing on GitHub.com |
copilot-proxy.githubusercontent.com |
HTTPS/WSS | GitHub Copilot proxy routing |
Public destinations require TLS and the default port. Host matching is exact: userinfo, trailing dots, suffix confusion, non-default ports, unsupported schemes, and alternate non-loopback IP forms are rejected. The URL parser canonicalizes integer, hexadecimal, octal, and shortened IPv4 forms before the loopback decision.
Redirects cannot cross origins. This prevents credentials from following a
redirect to another allowlisted host. New transport names fail closed; tests in
packages/core/test/outbound.test.ts are the protocol-conformance gate for new
callback/transport types.
The policy trusts the operating system resolver and TLS validation for the
fixed GitHub hostnames. Arbitrary DNS names are never accepted merely because
they resolve to loopback, which removes the application-level DNS-rebinding
case. localhost and literal loopback addresses are the only local names.
Node processes append to:
$XDG_STATE_HOME/opencode/security/outbound.jsonl
The directory is 0700; the file is 0600. Records contain only:
time, decision, destination category, transport, reason
They never contain a URL, URL path, prompt, code, headers, token, account,
session/project identifier, or request/response body. Blocked hosts are recorded
only as destination: "blocked".
The workplace product removes these production graph edges:
- Sentry runtime capture and source-map upload plugins
- OpenTelemetry log/trace exporters and AI SDK telemetry configuration
- Chromium net-log capture and Electron crash-dump collection
- hosted UI proxying to
app.opencode.ai - session share/upload/sync workers and automatic share behavior
- CostasCode Console account/provider integration and remote managed config
- CostasCode Go upsell/account links
- desktop and CLI update checks, downloads, and updater IPC/menu actions
- mDNS/Bonjour discovery and non-loopback server options
- WSL remote sidecars and desktop custom-protocol deep links
- models.dev background refresh
- models.dev build-time fetches and provider-icon downloads; builds use the
checked-in, hosted-provider-free
packages/schema/models-api.jsonsnapshot - runtime ripgrep and language-server downloads
- npm-on-demand plugins and automatic project plugin execution
- the Anomaly-hosted
ghostty-webGit dependency; the terminal uses the integrity-lockedghostty-webnpm release from the upstream Coder project - hosted deployment, analytics/stats, Discord notification, and upstream publishing workflows
The security static gate rejects reintroduction of the removed packages, endpoints, wildcard listener/CSP settings, and unguarded Electron external-link calls.
Server.listen rejects every hostname except 127.0.0.1, localhost, and
::1. CLI configuration cannot add CORS origins or enable discovery.
The web/desktop server picker also canonicalizes stored and newly entered URLs
through the same outbound policy, discards stale remote entries, and visibly
states that workplace builds accept loopback servers only.
Before routing, the server:
- parses and validates the
Hostheader as loopback; - parses
Originexactly and permits only loopback web origins or the exact Electron originoc://renderer; - rejects hostile browser origins before a mutation executes;
- applies HTTP Basic authentication when configured;
- requires a scoped, random, single-use, 60-second ticket for browser PTY WebSocket connections; and
- validates WebSocket Origin and Host through the same exact-origin helpers.
The Electron sidecar always uses a random in-memory password and binds to
127.0.0.1. No password is persisted in renderer storage. CLI listeners are
loopback-only; callers that need same-user process isolation should also set
OPENCODE_SERVER_PASSWORD.
The desktop renderer uses:
contextIsolation: truenodeIntegration: falsesandbox: truewebSecurity: true- no
<webview>tag - no insecure mixed content
- a local
oc://rendererprotocol with canonical path containment - a restrictive CSP, permissions policy, referrer policy, COOP/CORP, and
nosniff
Top-level navigation is limited to the exact renderer origin. New windows are
always denied; approved GitHub links are opened externally after outbound
validation. Every IPC handler verifies the sender frame is the exact packaged
renderer origin (or the exact local development origin in an unpackaged build).
ELECTRON_RENDERER_URL is ignored in packaged applications and must resolve to
an HTTP(S) loopback origin in development.
Store filenames use a strict basename grammar, picked-file reads are scoped to
the renderer and token, and attachment file identity is rechecked after the
picker to prevent symlink/replace races.
Production builds do not enable remote debugging, source-map upload, crash upload, update polling, or custom protocol deep links. Developer tools are not exposed in the product menu.
macOS packages disable arbitrary network loads, retain only explicit loopback
HTTP exceptions for the local sidecar, remove inherited camera, microphone, and
Bluetooth usage declarations, and grant only Electron's required JIT
entitlement. Beta and production packages fail before packing unless Developer
ID signing and complete notarization inputs are present. Unsigned local macOS
packages require the explicit package:mac:dev command.
Relative filesystem reads resolve both the lexical path and real path beneath
the active Location. Mutations use LocationMutation to detect relative escape,
symlink escape, and non-directory ancestors before permission evaluation.
Worktree removal/reset:
- canonicalizes the requested directory;
- requires it to be beneath the per-project managed worktree root;
- requires an exact entry from
git worktree list; - never recursively deletes an unknown directory;
- passes Git arguments as arrays rather than shell strings;
- does not run repository-provided startup scripts; and
- resets only from locally available refs and submodule objects (
--no-fetch).
Project-copy names accept one safe path segment only. Desktop store cleanup and custom protocol file loading reject traversal. Runtime archive download and extraction paths were removed with the updater/LSP/ripgrep downloaders.
An untrusted repository may still provide instructions, agents, commands, and skills because these are core harness capabilities. They are prompt input, not a sandbox boundary. Tool permissions remain mandatory user-attention controls.
Project-local plugins never execute. Registry plugin installation is disabled. Only an already-installed file plugin declared by trusted global configuration may load.
Project-local JavaScript/TypeScript tools are also executable plugins in practice, so they are disabled by default. A user who has reviewed a repository may opt in for that process with:
OPENCODE_ALLOW_PROJECT_TOOLS=1
Formatter configuration is also executable: formatter commands run automatically after edits. Repository formatter settings are therefore ignored. Built-in and custom formatters remain available from user-global, explicitly process-selected, or managed configuration; there is no repository trust opt-in until the product has a concrete persisted trust decision.
Repository reference settings are ignored for the same open-time side-effect reason. Trusted global references remain available. Git references additionally accept only GitHub HTTPS URLs and GitHub shorthand; arbitrary hosts, SSH/SCP forms, userinfo, and non-default ports are rejected before Git cache work starts.
Language servers can execute repository-local binaries and are likewise disabled by default. Reviewed repositories may opt in with:
OPENCODE_ALLOW_LOCAL_LSP=1
Opted-in language servers receive an environment with credential, proxy, dynamic-loader, and process-injection variables removed. Language-server downloads remain permanently disabled.
Remote MCP URLs must be loopback. Local stdio MCP is disabled by default and requires the user-level environment opt-in:
OPENCODE_ALLOW_LOCAL_MCP=1
Repository config cannot set that process-level decision. An opted-in MCP child must stay inside the project working directory after realpath resolution and receives a minimal environment; loader and proxy injection variables are stripped.
MCP OAuth callbacks bind to 127.0.0.1, require the exact callback path and
Host header, accept GET only, reject occupied callback ports, expire after five
minutes, and consume state once.
CostasCode applies a 0077 process umask. Data/config/state/log directories are
0700. SQLite, auth JSON, audit logs, exported debug archives, and related
state files are 0600.
Legacy provider credentials are replaced atomically in auth.json; V2
credentials live in the permission-restricted SQLite store. Desktop renderer
storage never receives provider credentials. GitHub Copilot authentication uses
the CostasCode OAuth application's device flow with the read:user scope. MCP
token expiry metadata is enforced before reuse.
The cross-platform CLI does not silently fall back from an unavailable OS keychain to plaintext renderer storage. This fork uses its explicit permission-restricted local credential store. Deployments requiring hardware-backed storage should inject short-lived credentials through the process environment and clear them after use.
Local logs recursively redact authorization, cookies, credentials, secrets, passwords, tokens, API keys, prompts, content/body/header fields, URL/path fields, account/session identifiers, email, URL text, bearer/basic values, and common absolute filesystem paths. Sidecar stdout/stderr is represented by byte count only. Debug-log export is always a user action.
CostasCode uses OpenCode's direct GitHub Copilot adapter for model discovery and inference. The experimental Copilot SDK bridge remains in source for isolated testing, but is not registered as the default provider because the SDK owns an agent/session loop rather than exposing a transparent inference transport.
The parallel inference integration must:
- provide an explicit loopback or exact GitHub Copilot
baseURL; - use the application
fetch/Effect HTTP layer orOutbound.guardedFetch; - declare any new GitHub hostname in the matrix and add policy tests before use;
- never synthesize enterprise/customer-controlled hostnames;
- never follow cross-origin redirects with credentials; and
- keep prompts, code, headers, and identifiers out of audit records.
Generic provider/model configuration, model settings, protocol hooks, and web search tool hooks remain present. A configured endpoint outside the matrix fails explicitly before provider execution.
Tests must run from package directories.
bun run lint
bun run security:check
bun run security:attest --app packages/desktop/dist/mac-arm64/CostasCode.app
bun run security:sbom
bun run script/turbo.ts typecheck
(cd packages/core && bun test)
(cd packages/core && bun typecheck)
(cd packages/opencode && bun test)
(cd packages/opencode && bun run test:httpapi)
(cd packages/opencode && bun typecheck && bun run build)
(cd packages/app && bun test && bun typecheck && bun run build)
(cd packages/desktop && bun test && bun typecheck && bun run build)script/security/sbom.ts emits deterministic CycloneDX 1.6 JSON from the
locked workspace and package graph. CI generates it twice and requires byte
identity.
script/security/attest-package.ts inspects the production dependency closure
and packaged app.asar, rejects telemetry/updater/hosted-share and raw
network-client signatures, checks effective macOS entitlements, ATS, bundle
identity, and URL schemes, and emits SHA-256 hashes for every packaged file
plus a canonical artifact-tree hash. It writes the report before failing.
script/turbo.ts is the only repository Turbo entrypoint. It sets
TURBO_TELEMETRY_DISABLED=1 and DO_NOT_TRACK=1 before starting Turbo.
For reproducibility checks:
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
bun run security:manifest --output /tmp/build-a.json
# rebuild from a clean worktree with the same lockfile and environment
bun run security:manifest --output /tmp/build-b.json
cmp /tmp/build-a.json /tmp/build-b.jsonThe repository has no automatic product publishing or updater feed. A release is a manual human decision after security, generated-client, package, integration, desktop, SBOM, and reproducibility gates pass. Generated bundles and the lockfile must be scanned for removed endpoint/dependency names and credential patterns before distribution.
Upstream is anomalyco/opencode, normally branch dev. Product changes belong
only on copilot-workplace.
- Fetch upstream into a temporary sync branch; never merge product changes
into local
dev/main. - Inventory upstream changes to networking, providers, auth, config, plugins, MCP, filesystem, desktop, build scripts, and dependencies.
- Port upstream commits into
copilot-workplacewithout restoring cloud, telemetry, updater, remote-listener, or auto-install behavior. - If Protocol or Server HttpApi changed, run
bun run generatefrompackages/client; never edit generated clients directly. - Run the static workplace gate before tests so a newly introduced endpoint or exporter fails early.
- Regenerate the SBOM and compare dependency deltas.
- Run the full gates above and review the final diff specifically for raw network clients and spawned subprocesses.
- The permission system is not an OS sandbox. Run hostile agent work in a VM or container.
- User-approved shell/terminal commands, Git commands, and explicitly opted-in local MCP, project-tool, and language-server processes are user-controlled. They can perform their own networking outside the application-owned policy.
- LLM data handling on the allowed GitHub Copilot service is governed by the organization’s GitHub/Copilot policy.
- Dormant hosted-service source directories excluded from the Bun workspace are retained only to reduce upstream-sync conflict. They are not a product or release input.
- A same-user malicious local process can read another same-user process’s memory and interact with loopback services unless OS isolation or a server password is used.
Report vulnerabilities privately through the SlowGreek/opencode GitHub Security Advisory interface. Do not include credentials, prompts, proprietary code, or unredacted logs.