Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Related prior requests. #6361, #7163, and #15234, plus PR #16104 (closed, not merged), touch parts of this problem. We searched issues and PRs and did not find this exact combined design: per-project and per-session restricted configuration profiles with session-preserving transitions. Links are under Related.
Design review requested before implementation
This issue is a design review, not an implementation report. Nothing described here is implemented or tested yet. We ask maintainers to review the design before any implementation starts.
Summary
Add per-project and per-session configuration restrictions to opencode. A project runs in one of two persisted modes: Unrestricted, which loads configuration as today, and Restricted, which skips selected configuration sources. Each session records its own actual mode and profile, and restrictions can change mid-session without closing the session or losing the visible transcript. Everything ships behind an experimental flag.
Problem and motivation
opencode automatically loads configuration from the working directory: the project config file, instructions, agents, commands, skills, and plugins. Anyone with write access to a repository controls all of it. Opening a project you did not write means trusting all of it today.
- A cloned repository can auto-execute MCP servers or plugins, and its instruction files can steer the model.
- Users cannot inspect a project safely first, and they cannot run restricted review and unrestricted work as parallel sessions on the same server.
- Teams sync configuration through git. A machine-specific or path-specific grant stored in that config would reach every clone.
- Changing trust today means restarting with different settings. Users want to change it without forced session closes or lost visible history.
- Scripts, CI, and Docker need a deterministic, non-interactive mode.
This feature is a trust boundary for configuration loading. It is not an absolute guarantee against remote code execution or prompt injection; see Limitations.
Prior art: VS Code Workspace Trust
VS Code ships Workspace Trust. The official guides are the reference for its behavior:
- User guide: https://code.visualstudio.com/docs/editing/workspaces/workspace-trust
- Extension author guide (restricted-mode API rules): https://code.visualstudio.com/api/extension-guides/workspace-trust
| Aspect | VS Code Workspace Trust | This proposal |
|---|---|---|
| Trusted vs restricted result | Trusting a folder re-enables tasks, debugging, terminal, extensions, and workspace settings. Restricted mode keeps them off. | Unrestricted loads the six project sources as today. Restricted skips the sources the profile turns off. The usual tools keep running under the existing permission system; we add no sandbox. |
| Restricted banner and prompting | With security.workspace.trust.startupPrompt: never, VS Code never asks; the window only shows the restricted banner. |
The one-time onboarding ask resolves the first choice, or the configured default choice (restrict or unrestrict) applies. There is no perpetual banner-only state. |
| Scope and inheritance | Trust is keyed by workspace URI and inherited from parent folders. | State is keyed by Project.ID and Session.ID on one server. Clones and worktrees with the same Project.ID on that server inherit; a different machine or database runs its own onboarding. Sessions on the same server stay independent. |
| What is restricted | AI agents, tasks, debug, terminal, selected settings, and extensions. | Configuration sources only — six whole-source keys. The usual tools keep their existing permissions. |
| Capability model | Extensions declare restrictedConfigurations and get full, limited, or no capability. |
One on/off switch per source category. A source loads whole or not at all; there is no per-item capability negotiation. |
| Storage | Per-user application storage on the client machine. | The opencode server's existing SQLite database, as typed records. There is no universal cross-process stop: an unrestricted process elsewhere is not stopped by this design. |
| Applying a change | Extension host restart or window reload. | The session survives: profile-graph retirement, a tracked outstanding-work drain, and a hard provider-context reset under a new context_revision. |
The differences follow from OpenCode's shape: long-lived sessions, MCP servers and plugins loaded from configuration, and a CLI and TUI that run headless. We do not claim undocumented parity with VS Code beyond the core trust idea, and we do not claim superiority.
Design
Two modes, one transient ask
There are exactly two persisted modes:
- Unrestricted — profile
ALL_ON. All six sources load, as today. - Restricted — any partial profile, or
ALL_OFF. The sources the profile turns off do not load.
The onboarding ask is a transient state only. It is never stored and never becomes a third mode. State is keyed by the normalized profile, not by a path. Lock and "R" icons in this issue are candidate visual ideas only; showing the effective sources of a partially restricted profile needs further design exploration.
The six restricted sources
Restricted mode tracks six keys:
| Key | What it covers |
|---|---|
opencode_json |
The whole project config: the direct file plus supplementary config. |
instructions |
Project instruction files, boot-time and read-time AGENTS-style files. |
agents |
Project agents, whole definitions: system prompt, model, request options, permissions. |
commands |
Project commands, whole activation. |
skills |
Project skills, whole activation. |
plugins |
Project plugins, whole activation. |
opencode_jsonaccepts every declaration the config supports: providers, models, MCP servers, permissions, agents, plugins, remote instructions, and more. When this key is off, the file is not read, no preparse substitution runs, and nothing in it activates. There is no separate MCP or provider toggle inside the JSON; MCP servers declared there are covered by this key.- The
.opencode,.claude, and.agentsdirectories are classified by their actual consumers under these keys. - Built-in
read,edit, andshelltools keep running under the existing permission system. There is no sandbox. Custom executable tools come from plugins, not from a.opencode/toolsdirectory. - A project cannot enable this feature for itself through configuration.
- The existing
OPENCODE_DISABLE_PROJECT_CONFIGenvironment variable stays a stricter, separate switch above this feature. - Managed policy is unchanged.
Storage: typed state in the existing database
Restriction state lives in the opencode server's existing SQLite database as typed records. There is no trust JSON file: a JSON sidecar in the project would sit inside the directory an attacker controls, and it could not express typed server state.
- The normalized profile record is called
restricted_profile: a mode plus the on/off state of the six keys. Normalization means two profiles with the same meaning are the same record. - Project record: an editable
restricted_profileplus a revision. It is the default for new standalone sessions. - Session record: the durable actual mode and profile,
mode_revision,context_revision, andorigin. Origin is one ofinherited,project,session, orcli_override, and it changes only through an explicit transition. - Clones and worktrees with the same
Project.IDon the same server inherit the project state. A different machine or database runs its own onboarding. - Plugins see the actual Session profile. They must not assume it equals the Project profile; that equality is false in general.
- Sessions that predate this feature — and all archived sessions — are marked Unrestricted at revision 0. They are not closed. A session waiting on the onboarding
askpauses; the default-only path resumes it under its old Unrestricted state. Bulk operations never change archived sessions.
Onboarding: one-time, per project
Onboarding is one-time per project, not a perpetual fallback. A sibling experimental flag, restriction_onboarding, sets unconfigured_projects to one of ask (default), restrict, or unrestrict, with a prefill profile of ALL_OFF:
ask— a mandatory project dialog opens with an editable prefill. The saved choice becomes the Project's actual restrictions.restrict— first onboarding persists the Project as Restricted with the partial set chosen.unrestrict— first onboarding is an explicit host opt-in toALL_ON.
Newly added projects follow the same policy. Later config changes apply only to not-yet-onboarded projects; existing projects and sessions are never updated or drained by a config change. After onboarding, a new session defaults to the Project's restrictions, not to global defaults. Nothing outside this onboarding treats ask as a persisted mode.
Upward config lookup is off by default
An experimental global/system/MDM/host setting, discover_project_config_upwards, defaults to false.
- Lookup inside the selected location, through the Project root, is unchanged. Monorepos keep working.
- With
false, no configuration source above the Project root is read, in either mode. - With
true, the legacy root search runs, but only for categories the profile enables. - A project cannot enable the setting for itself.
- Designated global configuration is unaffected.
Changing this default is breaking, and we flag it for maintainer approval.
GUI and TUI
- The current mode shows in the Composer next to the agent and model chips, in a hover/focus Popover, and in the mobile Drawer.
- The TUI shows it in
PromptMetadataRow, with a key-open detail dialog that adapts to the terminal size. - The detail view shows the Project default versus the actual Session profile, the origin, differences, a pending transition, an error state, and actions.
- Settings → Projects gets a Project restrictions section. The Home manager fine-tunes mixed counts and bulk per-session outcomes.
- The new-session welcome offers "This session only" or "Update project default", with explicit fine tuning for the Restricted→Unrestricted direction.
- Active, archive, and inbox lists show restriction badges in both GUI and TUI. These are session-list badges, not per-source skipped badges.
- Any exact icon — lock, "R", or otherwise — is deferred to visual exploration, as requested.
Changing restrictions mid-session
A Project-level change always offers exactly two choices:
- New sessions only — current sessions keep running. Nothing changes for them and nothing drains.
- Apply current — every unarchived session transitions, including descendants. Per the session search results, archived sessions are unchanged.
Applying current offers two timings:
- Wait for a step boundary — the current LLM step finishes, nothing new starts, then tracked jobs cancel or drain.
- Immediate — interrupt,
awaitIdle, stop or wait for jobs and shells, plus a tracked registry drain.
A pending transition is reported with a typed status and a deadline. A session is never force-closed.
A mode change, or a change to any single source, writes a durable RestrictionsChanged event with the old and new normalized profiles and revisions. The Session ID and the full visible transcript survive, but all old provider context is hard reset in both directions: AGENTS content, synthetic messages, user and assistant messages, summaries and compaction, and native windows. The UI explicitly warns that model memory resets.
- A fork gets its own durable context barrier, so pre-barrier context is never reinjected. The baseline is copied only when the fork has no other context.
- Subagents inherit the parent profile.
- A cross-project move applies the new profile and a new barrier.
- Guards cover admissions: prompt, resume, title, generate, compaction, skill, fork, subagent, move, and the common LLM step boundary. Normal queued work continues after the change; a queued move is cancelled.
Safety mechanisms kept
- Graphs are keyed by
(Location.Ref, normalized profile content). The revision is excluded from the key, so identical profiles share graph content and different profiles stay isolated. - Gates run at source acquisition, before any read, fetch, import, or process.
- Retirement applies only when the last borrower of a profile stops — never a generic reload.
- A per-Session
outstanding-workregistry covers main, title, generate, compaction, Shell, subagent, and notification writers. They are cancelled or drained within a finite deadline. - Every writer carries
sessionIDpluscontext_revision. A stale writer is fenced before theRestrictionsChangedbarrier. Drained, cancelled, or archived work never produces new model context. awaitIdlealone is not sufficient.- Moving toward Restricted persists the restriction early and fails closed. After a cleanup failure or
context_revisionmismatch, an old Unrestricted state is never resumed. - Unrestricted startup is staged: it builds and drains first, then commits a marker. A failure leaves a working Restricted state.
- Writes use an expected-revision CAS.
- An execution interrupt is process-local. When a durable claim is held by another process, the result is
ownership_conflict, not a fake global stop. - Local managed and standalone/private TUI deployments are supported. Remote Workspaces are not.
- The default-only path does no cross-process drain.
- There is no rollback of external code effects, no cleanup of detached processes, and no universal global stop.
Worktree startup
A stored worktree startup command may run when the Project is Unrestricted. Restricted or awaiting-choice projects block startup and record a pending state, with explicit retry after the Project becomes Unrestricted. A Session-only override never changes that Project-level baseline. Exactly-once startup after a crash is not promised.
CLI
- Exact flag names:
--unrestrictedsetsALL_ON;--restrictedsetsALL_OFF. Both are strict: they set the strict endpoint profiles, not the configured partial profile. Invalid combinations are parse errors. - The flags apply to this invocation and its new Session only. They are never written as the Project default. The actual Session record stores
cli_overrideas origin, and that record is kept in the archive. - They do not touch
--autoor headless deny semantics. Setting theopencodeenvironment variables does not bypass managed policy. - Managed, standalone, and
--serverdeployments must be capability-compatible; otherwise the server returns a typed mismatch error. - A headless run with no flag, while an
askis pending, exits with a descriptive non-zero status and remediation text. It does not wait on a modal dialog. - Resuming a session with an incompatible flag is an error that requires an explicit transition. It never silently resets.
- ACP and mini clients are deferred.
Flag and delivery
The feature flag is project_restrictions, initially disabled in releases. Its value is resolved before project discovery, only from safe global/system/MDM/host sources, and is then fixed for the server process until restart. A project cannot enable the flag itself. The server reports its actual capability.
The experimental API lands first, then we run bun run generate in packages/client. Generated client files are never edited by hand.
Delivery is planned in three slices under the single flag:
- Dormant foundation — source gates, profile graphs, upward lookup, database migration, tests.
- Dormant runtime — session history, context fences,
outstanding-work, transitions, startup, API tests. - Complete client — GUI, TUI, CLI, and the flag enabled last.
Each slice is complete for its scope. The feature is not called complete until slice 3; a UI-only partial state is not a completion claim.
Planned test coverage: stale completions, generate and other outstanding model work, fork context barriers, source acquisition gates, above-root discovery defaults, prevention of self-authorization, no retroactive onboarding inheritance, revision CAS, crash recovery, external ownership conflicts, and local GUI/TUI integration. No tests exist yet.
Limitations
- Enabling
opencode_jsonintentionally accepts all its declarations, including MCP, providers, and plugins. General file content can still steer the model. Partial-profile choices remain the user's responsibility. This is not a sandbox. - Previously caused side effects and detached processes cannot be undone. A transition cannot undo what already ran.
- Remote Workspaces are outside the initial scope. There is also no universal immediate-stop guarantee across independent processes.
- Exactly-once behavior across a crash is not guaranteed.
Questions for maintainers
- Naming — are the final names right:
project_restrictions,restriction_onboarding,discover_project_config_upwards,restricted_profile, and the six source keys? - Scope — are six whole-source keys the right scope, or should more or fewer sources be restricted?
- Onboarding — are the
askdefault, the prefill profile, and the one-time-per-project policy right? - Model reset — is the hard provider-context reset on every transition acceptable, given the explicit warning?
- Upward lookup — do maintainers approve the breaking
discover_project_config_upwardsdefault offalse? - Process model — is
ownership_conflictthe right result for a cross-process claim conflict? - Tracking — what should the pending-transition deadline and typed reporting look like?
JSON-declared MCP servers are covered by opencode_json; ownership_conflict specifically concerns execution claims owned by another process.
Related
- No trusted workspace functionality leads to arbitrary commands execution on startup #6361
- Security Issue - Autoloading Repository Plugins #7163
- [FEATURE]: Workspace Trust Dialog & Universal Permission Hooks (Mitigating Supply Chain RCE) #15234
- fix(core): gate project .opencode tools behind permission prompts #16104 (PR, closed, not merged)
Activity
opencodejr commentedon Nov 13, 2026
Thanks for the detailed design, @akaRem. As I understand it, you're proposing per-project and per-session restricted configuration profiles: opening a repository you didn't write would no longer automatically load its
opencode.json, instructions, agents, commands, skills or plugins. There would be a one-time choice for each project, and you could switch modes mid-session without losing the visible transcript. All of it would sit behind an experimental flag.Maintainers will look at the design. I can't say whether or when it would be accepted. They will probably weigh the breaking part (turning off upward config lookup by default) separately from the rest.
Related issues, for context:
opencode.jsonrunning at startup without consent (closed).opencodeloading automatically (closed).opencodetools behind a permission prompt (closed, not merged)